/*! * VisualEditor UserInterface PositionedTargetToolbar class. * * @copyright 2011-2019 VisualEditor Team and others; see http://ve.mit-license.org */ /** * UserInterface positioned target toolbar. * * @class * @extends ve.ui.TargetToolbar * * @constructor * @param {ve.init.Target} target * @param {Object} [config] Configuration options * @cfg {boolean} [floatable] Toolbar can float when scrolled off the page */ ve.ui.PositionedTargetToolbar = function VeUiPositionedTargetToolbar( target, config ) { config = config || {}; // Parent constructor ve.ui.PositionedTargetToolbar.super.apply( this, arguments ); // Change default overlay to be this.$bar, instead of this.$element (T209192) // TODO: Upstream to OOUI if ( !config.$overlay ) { this.$overlay = this.$bar.append( this.$popups ); } // Properties this.floating = false; this.floatable = !!config.floatable; this.$window = $( this.getElementWindow() ); this.elementOffset = null; this.toolbarScrollOffset = mw.config.get( 'wgVisualEditorToolbarScrollOffset', 0 ); this.onWindowScrollThrottled = ve.throttle( this.onWindowScroll.bind( this ), 250 ); // Initialization this.$element.addClass( 've-ui-positionedTargetToolbar' ); }; /* Inheritance */ OO.inheritClass( ve.ui.PositionedTargetToolbar, ve.ui.TargetToolbar ); /* Methods */ /** * @inheritdoc */ ve.ui.PositionedTargetToolbar.prototype.setup = function ( groups, surface ) { var toolbarDialogs = surface.getToolbarDialogs(); // Parent method ve.ui.PositionedTargetToolbar.super.prototype.setup.apply( this, arguments ); if ( this.position === 'bottom' ) { this.$bar.prepend( toolbarDialogs.$element ); } else { this.$bar.append( toolbarDialogs.$element ); } toolbarDialogs.connect( this, { opening: 'onToolbarDialogsOpeningOrClosing', closing: 'onToolbarDialogsOpeningOrClosing' } ); if ( this.isFloatable() ) { ve.addPassiveEventListener( this.$window[ 0 ], 'scroll', this.onWindowScrollThrottled ); } }; /** * @inheritdoc */ ve.ui.PositionedTargetToolbar.prototype.detach = function () { this.unfloat(); // Events if ( this.getSurface() ) { this.getSurface().getToolbarDialogs().disconnect( this ); this.getSurface().getToolbarDialogs().clearWindows(); } ve.removePassiveEventListener( this.$window[ 0 ], 'scroll', this.onWindowScrollThrottled ); // Parent method ve.ui.PositionedTargetToolbar.super.prototype.detach.apply( this, arguments ); }; /** * @inheritdoc * * While toolbar floating is enabled, * the toolbar will stick to the top of the screen unless it would be over or under the last visible * branch node in the root of the document being edited, at which point it will stop just above it. */ ve.ui.PositionedTargetToolbar.prototype.onWindowResize = function () { ve.ui.Toolbar.super.prototype.onWindowResize.call( this ); // Update offsets after resize (see #float) this.calculateOffset(); if ( this.floating ) { this.$bar.css( { left: this.elementOffset.left, right: this.elementOffset.right } ); } this.onViewportResize(); }; /** * Calculate the left and right offsets of the toolbar */ ve.ui.PositionedTargetToolbar.prototype.calculateOffset = function () { this.elementOffset = this.$element.offset(); this.elementOffset.top = this.toolbarScrollOffset; this.elementOffset.right = document.documentElement.clientWidth - this.$element[ 0 ].offsetWidth - this.elementOffset.left; }; /** * Get height of the toolbar while floating * * @return {number} Height of the toolbar */ ve.ui.PositionedTargetToolbar.prototype.getHeight = function () { return this.height; }; /** * Get toolbar element's offsets * * @return {Object} Toolbar element's offsets */ ve.ui.PositionedTargetToolbar.prototype.getElementOffset = function () { if ( !this.elementOffset ) { this.calculateOffset(); } return this.elementOffset; }; /** * Float the toolbar. */ ve.ui.PositionedTargetToolbar.prototype.float = function () { if ( !this.floating ) { this.height = this.$element[ 0 ].offsetHeight; // When switching into floating mode, set the height of the wrapper and // move the bar to the same offset as the in-flow element this.$element .css( 'height', this.height ) .addClass( 've-ui-toolbar-floating' ); this.$bar.css( { left: this.elementOffset.left, right: this.elementOffset.right, top: this.elementOffset.top } ); this.floating = true; this.emit( 'resize' ); this.onViewportResize(); } }; /** * Reset the toolbar to it's default non-floating position. */ ve.ui.PositionedTargetToolbar.prototype.unfloat = function () { if ( this.floating ) { this.height = 0; this.$element .css( 'height', '' ) .removeClass( 've-ui-toolbar-floating' ); this.$bar.css( { left: '', right: '', top: '' } ); this.floating = false; this.emit( 'resize' ); this.onViewportResize(); } }; /** * Check if the toolbar is floating * * @return {boolean} The toolbar is floating */ ve.ui.PositionedTargetToolbar.prototype.isFloating = function () { return this.floating; }; /** * Check if the toolbar can float * * @return {boolean} The toolbar can float */ ve.ui.PositionedTargetToolbar.prototype.isFloatable = function () { return this.floatable; }; /** * Handle windows opening or closing in the toolbar window manager. * * @param {OO.ui.Window} win * @param {jQuery.Promise} openingOrClosing * @param {Object} data */ ve.ui.PositionedTargetToolbar.prototype.onToolbarDialogsOpeningOrClosing = function ( win, openingOrClosing ) { var width, margin, $surface = this.getSurface().$element, transitionDuration = OO.ui.theme.getDialogTransitionDuration(), toolbar = this; // win.isOpened before promise means we are closing if ( win.constructor.static.position === 'side' && win.isOpened() ) { // First closing transition $surface.css( $surface.css( 'direction' ) === 'rtl' ? 'margin-left' : 'margin-right', '' ); win.$element.css( 'width', '' ); } openingOrClosing.then( function () { var originalMargin; if ( win.constructor.static.position === 'side' ) { // win.isOpened after promise means we are opening if ( win.isOpened() ) { margin = $surface.css( 'direction' ) === 'rtl' ? 'margin-left' : 'margin-right'; originalMargin = parseFloat( $surface.css( margin ) ); width = win.getSizeProperties().width; toolbar.getSurface().$element .addClass( 've-ui-surface-toolbarDialog-side' ) .css( margin, width + originalMargin ); win.$element.css( 'width', width ); } else { // Second closing transition toolbar.getSurface().$element.removeClass( 've-ui-surface-toolbarDialog-side' ); } toolbar.onViewportResize(); setTimeout( function () { toolbar.onViewportResize(); toolbar.getSurface().getView().emit( 'position' ); }, transitionDuration ); toolbar.getSurface().getView().emit( 'position' ); } // Wait for window transition setTimeout( function () { if ( toolbar.floating ) { // Re-calculate height toolbar.unfloat(); toolbar.float(); } }, transitionDuration ); } ); }; /** * Handle the visible part of the surface viewport change dimensions */ ve.ui.PositionedTargetToolbar.prototype.onViewportResize = function () { var win, viewportDimensions, toolbarDialogs, surface = this.getSurface(); if ( !surface ) { return; } toolbarDialogs = surface.getToolbarDialogs(); win = toolbarDialogs.getCurrentWindow(); if ( win && win.constructor.static.position === 'side' ) { viewportDimensions = surface.getViewportDimensions(); if ( viewportDimensions ) { toolbarDialogs.getCurrentWindow().$frame.css( 'height', Math.min( surface.getBoundingClientRect().height, viewportDimensions.height ) ); } } }; /** * Handle window scroll events */ ve.ui.PositionedTargetToolbar.prototype.onWindowScroll = function () { if ( !this.floating ) { this.onViewportResize(); } };