diff options
Diffstat (limited to 'vendor/oojs/oojs-ui/src/WindowManager.js')
-rw-r--r-- | vendor/oojs/oojs-ui/src/WindowManager.js | 675 |
1 files changed, 675 insertions, 0 deletions
diff --git a/vendor/oojs/oojs-ui/src/WindowManager.js b/vendor/oojs/oojs-ui/src/WindowManager.js new file mode 100644 index 00000000..4731011a --- /dev/null +++ b/vendor/oojs/oojs-ui/src/WindowManager.js @@ -0,0 +1,675 @@ +/** + * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation. + * Managed windows are mutually exclusive. If a new window is opened while a current window is opening + * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows + * themselves are persistent and—rather than being torn down when closed—can be repopulated with the + * pertinent data and reused. + * + * Over the lifecycle of a window, the window manager makes available three promises: `opening`, + * `opened`, and `closing`, which represent the primary stages of the cycle: + * + * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s + * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window. + * + * - an `opening` event is emitted with an `opening` promise + * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before + * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the + * window and its result executed + * - a `setup` progress notification is emitted from the `opening` promise + * - the #getReadyDelay method is called the returned value is used to time a pause in execution before + * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the + * window and its result executed + * - a `ready` progress notification is emitted from the `opening` promise + * - the `opening` promise is resolved with an `opened` promise + * + * **Opened**: the window is now open. + * + * **Closing**: the closing stage begins when the window manager's #closeWindow or the + * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins + * to close the window. + * + * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted + * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before + * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the + * window and its result executed + * - a `hold` progress notification is emitted from the `closing` promise + * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before + * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the + * window and its result executed + * - a `teardown` progress notification is emitted from the `closing` promise + * - the `closing` promise is resolved. The window is now closed + * + * See the [OOjs UI documentation on MediaWiki][1] for more information. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers + * + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation + * Note that window classes that are instantiated with a factory must have + * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name. + * @cfg {boolean} [modal=true] Prevent interaction outside the dialog + */ +OO.ui.WindowManager = function OoUiWindowManager( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.WindowManager.super.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.factory = config.factory; + this.modal = config.modal === undefined || !!config.modal; + this.windows = {}; + this.opening = null; + this.opened = null; + this.closing = null; + this.preparingToOpen = null; + this.preparingToClose = null; + this.currentWindow = null; + this.globalEvents = false; + this.$ariaHidden = null; + this.onWindowResizeTimeout = null; + this.onWindowResizeHandler = this.onWindowResize.bind( this ); + this.afterWindowResizeHandler = this.afterWindowResize.bind( this ); + + // Initialization + this.$element + .addClass( 'oo-ui-windowManager' ) + .toggleClass( 'oo-ui-windowManager-modal', this.modal ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.WindowManager, OO.ui.Element ); +OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter ); + +/* Events */ + +/** + * An 'opening' event is emitted when the window begins to be opened. + * + * @event opening + * @param {OO.ui.Window} win Window that's being opened + * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully. + * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument + * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete. + * @param {Object} data Window opening data + */ + +/** + * A 'closing' event is emitted when the window begins to be closed. + * + * @event closing + * @param {OO.ui.Window} win Window that's being closed + * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window + * is closed successfully. The promise emits `hold` and `teardown` notifications when those + * processes are complete. When the `closing` promise is resolved, the first argument of its value + * is the closing data. + * @param {Object} data Window closing data + */ + +/** + * A 'resize' event is emitted when a window is resized. + * + * @event resize + * @param {OO.ui.Window} win Window that was resized + */ + +/* Static Properties */ + +/** + * Map of the symbolic name of each window size and its CSS properties. + * + * @static + * @inheritable + * @property {Object} + */ +OO.ui.WindowManager.static.sizes = { + small: { + width: 300 + }, + medium: { + width: 500 + }, + large: { + width: 700 + }, + larger: { + width: 900 + }, + full: { + // These can be non-numeric because they are never used in calculations + width: '100%', + height: '100%' + } +}; + +/** + * Symbolic name of the default window size. + * + * The default size is used if the window's requested size is not recognized. + * + * @static + * @inheritable + * @property {string} + */ +OO.ui.WindowManager.static.defaultSize = 'medium'; + +/* Methods */ + +/** + * Handle window resize events. + * + * @private + * @param {jQuery.Event} e Window resize event + */ +OO.ui.WindowManager.prototype.onWindowResize = function () { + clearTimeout( this.onWindowResizeTimeout ); + this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 ); +}; + +/** + * Handle window resize events. + * + * @private + * @param {jQuery.Event} e Window resize event + */ +OO.ui.WindowManager.prototype.afterWindowResize = function () { + if ( this.currentWindow ) { + this.updateWindowSize( this.currentWindow ); + } +}; + +/** + * Check if window is opening. + * + * @return {boolean} Window is opening + */ +OO.ui.WindowManager.prototype.isOpening = function ( win ) { + return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending'; +}; + +/** + * Check if window is closing. + * + * @return {boolean} Window is closing + */ +OO.ui.WindowManager.prototype.isClosing = function ( win ) { + return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending'; +}; + +/** + * Check if window is opened. + * + * @return {boolean} Window is opened + */ +OO.ui.WindowManager.prototype.isOpened = function ( win ) { + return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending'; +}; + +/** + * Check if a window is being managed. + * + * @param {OO.ui.Window} win Window to check + * @return {boolean} Window is being managed + */ +OO.ui.WindowManager.prototype.hasWindow = function ( win ) { + var name; + + for ( name in this.windows ) { + if ( this.windows[ name ] === win ) { + return true; + } + } + + return false; +}; + +/** + * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process. + * + * @param {OO.ui.Window} win Window being opened + * @param {Object} [data] Window opening data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getSetupDelay = function () { + return 0; +}; + +/** + * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process. + * + * @param {OO.ui.Window} win Window being opened + * @param {Object} [data] Window opening data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getReadyDelay = function () { + return 0; +}; + +/** + * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process. + * + * @param {OO.ui.Window} win Window being closed + * @param {Object} [data] Window closing data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getHoldDelay = function () { + return 0; +}; + +/** + * Get the number of milliseconds to wait after the ‘hold’ process has finished before + * executing the ‘teardown’ process. + * + * @param {OO.ui.Window} win Window being closed + * @param {Object} [data] Window closing data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getTeardownDelay = function () { + return this.modal ? 250 : 0; +}; + +/** + * Get a window by its symbolic name. + * + * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be + * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3] + * for more information about using factories. + * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers + * + * @param {string} name Symbolic name of the window + * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error + * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory. + * @throws {Error} An error is thrown if the named window is not recognized as a managed window. + */ +OO.ui.WindowManager.prototype.getWindow = function ( name ) { + var deferred = $.Deferred(), + win = this.windows[ name ]; + + if ( !( win instanceof OO.ui.Window ) ) { + if ( this.factory ) { + if ( !this.factory.lookup( name ) ) { + deferred.reject( new OO.ui.Error( + 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory' + ) ); + } else { + win = this.factory.create( name ); + this.addWindows( [ win ] ); + deferred.resolve( win ); + } + } else { + deferred.reject( new OO.ui.Error( + 'Cannot get unmanaged window: symbolic name unrecognized as a managed window' + ) ); + } + } else { + deferred.resolve( win ); + } + + return deferred.promise(); +}; + +/** + * Get current window. + * + * @return {OO.ui.Window|null} Currently opening/opened/closing window + */ +OO.ui.WindowManager.prototype.getCurrentWindow = function () { + return this.currentWindow; +}; + +/** + * Open a window. + * + * @param {OO.ui.Window|string} win Window object or symbolic name of window to open + * @param {Object} [data] Window opening data + * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening. + * See {@link #event-opening 'opening' event} for more information about `opening` promises. + * @fires opening + */ +OO.ui.WindowManager.prototype.openWindow = function ( win, data ) { + var manager = this, + opening = $.Deferred(); + + // Argument handling + if ( typeof win === 'string' ) { + return this.getWindow( win ).then( function ( win ) { + return manager.openWindow( win, data ); + } ); + } + + // Error handling + if ( !this.hasWindow( win ) ) { + opening.reject( new OO.ui.Error( + 'Cannot open window: window is not attached to manager' + ) ); + } else if ( this.preparingToOpen || this.opening || this.opened ) { + opening.reject( new OO.ui.Error( + 'Cannot open window: another window is opening or open' + ) ); + } + + // Window opening + if ( opening.state() !== 'rejected' ) { + // If a window is currently closing, wait for it to complete + this.preparingToOpen = $.when( this.closing ); + // Ensure handlers get called after preparingToOpen is set + this.preparingToOpen.done( function () { + if ( manager.modal ) { + manager.toggleGlobalEvents( true ); + manager.toggleAriaIsolation( true ); + } + manager.currentWindow = win; + manager.opening = opening; + manager.preparingToOpen = null; + manager.emit( 'opening', win, opening, data ); + setTimeout( function () { + win.setup( data ).then( function () { + manager.updateWindowSize( win ); + manager.opening.notify( { state: 'setup' } ); + setTimeout( function () { + win.ready( data ).then( function () { + manager.opening.notify( { state: 'ready' } ); + manager.opening = null; + manager.opened = $.Deferred(); + opening.resolve( manager.opened.promise(), data ); + } ); + }, manager.getReadyDelay() ); + } ); + }, manager.getSetupDelay() ); + } ); + } + + return opening.promise(); +}; + +/** + * Close a window. + * + * @param {OO.ui.Window|string} win Window object or symbolic name of window to close + * @param {Object} [data] Window closing data + * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing. + * See {@link #event-closing 'closing' event} for more information about closing promises. + * @throws {Error} An error is thrown if the window is not managed by the window manager. + * @fires closing + */ +OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { + var manager = this, + closing = $.Deferred(), + opened; + + // Argument handling + if ( typeof win === 'string' ) { + win = this.windows[ win ]; + } else if ( !this.hasWindow( win ) ) { + win = null; + } + + // Error handling + if ( !win ) { + closing.reject( new OO.ui.Error( + 'Cannot close window: window is not attached to manager' + ) ); + } else if ( win !== this.currentWindow ) { + closing.reject( new OO.ui.Error( + 'Cannot close window: window already closed with different data' + ) ); + } else if ( this.preparingToClose || this.closing ) { + closing.reject( new OO.ui.Error( + 'Cannot close window: window already closing with different data' + ) ); + } + + // Window closing + if ( closing.state() !== 'rejected' ) { + // If the window is currently opening, close it when it's done + this.preparingToClose = $.when( this.opening ); + // Ensure handlers get called after preparingToClose is set + this.preparingToClose.done( function () { + manager.closing = closing; + manager.preparingToClose = null; + manager.emit( 'closing', win, closing, data ); + opened = manager.opened; + manager.opened = null; + opened.resolve( closing.promise(), data ); + setTimeout( function () { + win.hold( data ).then( function () { + closing.notify( { state: 'hold' } ); + setTimeout( function () { + win.teardown( data ).then( function () { + closing.notify( { state: 'teardown' } ); + if ( manager.modal ) { + manager.toggleGlobalEvents( false ); + manager.toggleAriaIsolation( false ); + } + manager.closing = null; + manager.currentWindow = null; + closing.resolve( data ); + } ); + }, manager.getTeardownDelay() ); + } ); + }, manager.getHoldDelay() ); + } ); + } + + return closing.promise(); +}; + +/** + * Add windows to the window manager. + * + * Windows can be added by reference, symbolic name, or explicitly defined symbolic names. + * See the [OOjs ui documentation on MediaWiki] [2] for examples. + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers + * + * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified + * by reference, symbolic name, or explicitly defined symbolic names. + * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an + * explicit nor a statically configured symbolic name. + */ +OO.ui.WindowManager.prototype.addWindows = function ( windows ) { + var i, len, win, name, list; + + if ( Array.isArray( windows ) ) { + // Convert to map of windows by looking up symbolic names from static configuration + list = {}; + for ( i = 0, len = windows.length; i < len; i++ ) { + name = windows[ i ].constructor.static.name; + if ( typeof name !== 'string' ) { + throw new Error( 'Cannot add window' ); + } + list[ name ] = windows[ i ]; + } + } else if ( OO.isPlainObject( windows ) ) { + list = windows; + } + + // Add windows + for ( name in list ) { + win = list[ name ]; + this.windows[ name ] = win.toggle( false ); + this.$element.append( win.$element ); + win.setManager( this ); + } +}; + +/** + * Remove the specified windows from the windows manager. + * + * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use + * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no + * longer listens to events, use the #destroy method. + * + * @param {string[]} names Symbolic names of windows to remove + * @return {jQuery.Promise} Promise resolved when window is closed and removed + * @throws {Error} An error is thrown if the named windows are not managed by the window manager. + */ +OO.ui.WindowManager.prototype.removeWindows = function ( names ) { + var i, len, win, name, cleanupWindow, + manager = this, + promises = [], + cleanup = function ( name, win ) { + delete manager.windows[ name ]; + win.$element.detach(); + }; + + for ( i = 0, len = names.length; i < len; i++ ) { + name = names[ i ]; + win = this.windows[ name ]; + if ( !win ) { + throw new Error( 'Cannot remove window' ); + } + cleanupWindow = cleanup.bind( null, name, win ); + promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) ); + } + + return $.when.apply( $, promises ); +}; + +/** + * Remove all windows from the window manager. + * + * Windows will be closed before they are removed. Note that the window manager, though not in use, will still + * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead. + * To remove just a subset of windows, use the #removeWindows method. + * + * @return {jQuery.Promise} Promise resolved when all windows are closed and removed + */ +OO.ui.WindowManager.prototype.clearWindows = function () { + return this.removeWindows( Object.keys( this.windows ) ); +}; + +/** + * Set dialog size. In general, this method should not be called directly. + * + * Fullscreen mode will be used if the dialog is too wide to fit in the screen. + * + * @chainable + */ +OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) { + // Bypass for non-current, and thus invisible, windows + if ( win !== this.currentWindow ) { + return; + } + + var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ), + sizes = this.constructor.static.sizes, + size = win.getSize(); + + if ( !sizes[ size ] ) { + size = this.constructor.static.defaultSize; + } + if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) { + size = 'full'; + } + + this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' ); + this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' ); + win.setDimensions( sizes[ size ] ); + + this.emit( 'resize', win ); + + return this; +}; + +/** + * Bind or unbind global events for scrolling. + * + * @private + * @param {boolean} [on] Bind global events + * @chainable + */ +OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) { + on = on === undefined ? !!this.globalEvents : !!on; + + var scrollWidth, bodyMargin, + $body = $( this.getElementDocument().body ), + // We could have multiple window managers open so only modify + // the body css at the bottom of the stack + stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ; + + if ( on ) { + if ( !this.globalEvents ) { + $( this.getElementWindow() ).on( { + // Start listening for top-level window dimension changes + 'orientationchange resize': this.onWindowResizeHandler + } ); + if ( stackDepth === 0 ) { + scrollWidth = window.innerWidth - document.documentElement.clientWidth; + bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0; + $body.css( { + overflow: 'hidden', + 'margin-right': bodyMargin + scrollWidth + } ); + } + stackDepth++; + this.globalEvents = true; + } + } else if ( this.globalEvents ) { + $( this.getElementWindow() ).off( { + // Stop listening for top-level window dimension changes + 'orientationchange resize': this.onWindowResizeHandler + } ); + stackDepth--; + if ( stackDepth === 0 ) { + $body.css( { + overflow: '', + 'margin-right': '' + } ); + } + this.globalEvents = false; + } + $body.data( 'windowManagerGlobalEvents', stackDepth ); + + return this; +}; + +/** + * Toggle screen reader visibility of content other than the window manager. + * + * @private + * @param {boolean} [isolate] Make only the window manager visible to screen readers + * @chainable + */ +OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) { + isolate = isolate === undefined ? !this.$ariaHidden : !!isolate; + + if ( isolate ) { + if ( !this.$ariaHidden ) { + // Hide everything other than the window manager from screen readers + this.$ariaHidden = $( 'body' ) + .children() + .not( this.$element.parentsUntil( 'body' ).last() ) + .attr( 'aria-hidden', '' ); + } + } else if ( this.$ariaHidden ) { + // Restore screen reader visibility + this.$ariaHidden.removeAttr( 'aria-hidden' ); + this.$ariaHidden = null; + } + + return this; +}; + +/** + * Destroy the window manager. + * + * Destroying the window manager ensures that it will no longer listen to events. If you would like to + * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method + * instead. + */ +OO.ui.WindowManager.prototype.destroy = function () { + this.toggleGlobalEvents( false ); + this.toggleAriaIsolation( false ); + this.clearWindows(); + this.$element.remove(); +}; |