3 * https://www.mediawiki.org/wiki/OOUI
5 * Copyright 2011–2024 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2024-12-05T17:34:41Z
16 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
17 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
20 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
21 * Please see the [OOUI documentation on MediaWiki][1] for more information
24 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
27 * @extends OO.ui.ButtonWidget
28 * @mixes OO.ui.mixin.PendingElement
31 * @param {Object} [config] Configuration options
32 * @param {string} [config.action=''] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
33 * @param {string[]} [config.modes=[]] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the
34 * action should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode}
35 * method for more information about setting modes.
36 * @param {boolean} [config.framed=false] Render the action button with a frame
38 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
39 // Configuration initialization
40 config = Object.assign( { framed: false }, config );
43 OO.ui.ActionWidget.super.call( this, config );
46 OO.ui.mixin.PendingElement.call( this, config );
49 this.action = config.action || '';
50 this.modes = config.modes || [];
55 this.$element.addClass( 'oo-ui-actionWidget' );
60 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
61 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
66 * Check if the action is configured to be available in the specified `mode`.
68 * @param {string} mode Name of mode
69 * @return {boolean} The action is configured with the mode
71 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
72 return this.modes.indexOf( mode ) !== -1;
76 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
80 OO.ui.ActionWidget.prototype.getAction = function () {
85 * Get the symbolic name of the mode or modes for which the action is configured to be available.
87 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
88 * Only actions that are configured to be available in the current mode will be visible.
89 * All other actions are hidden.
93 OO.ui.ActionWidget.prototype.getModes = function () {
94 return this.modes.slice();
97 /* eslint-disable no-unused-vars */
99 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that
101 * Actions can be made available for specific contexts (modes) and circumstances
102 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
104 * ActionSets contain two types of actions:
106 * - Special: Special actions are the first visible actions with special flags, such as 'safe' and
107 * 'primary', the default special flags. Additional special flags can be configured in subclasses
108 * with the static #specialFlags property.
109 * - Other: Other actions include all non-special visible actions.
111 * See the [OOUI documentation on MediaWiki][1] for more information.
113 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
116 * // Example: An action set used in a process dialog
117 * function MyProcessDialog( config ) {
118 * MyProcessDialog.super.call( this, config );
120 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
121 * MyProcessDialog.static.title = 'An action set in a process dialog';
122 * MyProcessDialog.static.name = 'myProcessDialog';
123 * // An action set that uses modes ('edit' and 'help' mode, in this example).
124 * MyProcessDialog.static.actions = [
126 * action: 'continue',
129 * flags: [ 'primary', 'progressive' ]
131 * { action: 'help', modes: 'edit', label: 'Help' },
132 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
133 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
136 * MyProcessDialog.prototype.initialize = function () {
137 * MyProcessDialog.super.prototype.initialize.apply( this, arguments );
138 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
139 * this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, ' +
140 * 'cancel, back) configured with modes. This is edit mode. Click \'help\' to see ' +
141 * 'help mode.</p>' );
142 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
143 * this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget ' +
144 * 'is configured to be visible here. Click \'back\' to return to \'edit\' mode.' +
146 * this.stackLayout = new OO.ui.StackLayout( {
147 * items: [ this.panel1, this.panel2 ]
149 * this.$body.append( this.stackLayout.$element );
151 * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
152 * return MyProcessDialog.super.prototype.getSetupProcess.call( this, data )
154 * this.actions.setMode( 'edit' );
157 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
158 * if ( action === 'help' ) {
159 * this.actions.setMode( 'help' );
160 * this.stackLayout.setItem( this.panel2 );
161 * } else if ( action === 'back' ) {
162 * this.actions.setMode( 'edit' );
163 * this.stackLayout.setItem( this.panel1 );
164 * } else if ( action === 'continue' ) {
165 * return new OO.ui.Process( () => {
169 * return MyProcessDialog.super.prototype.getActionProcess.call( this, action );
171 * MyProcessDialog.prototype.getBodyHeight = function () {
172 * return this.panel1.$element.outerHeight( true );
174 * const windowManager = new OO.ui.WindowManager();
175 * $( document.body ).append( windowManager.$element );
176 * const dialog = new MyProcessDialog( {
179 * windowManager.addWindows( [ dialog ] );
180 * windowManager.openWindow( dialog );
184 * @mixes OO.EventEmitter
187 * @param {Object} [config] Configuration options
189 OO.ui.ActionSet = function OoUiActionSet( config ) {
190 // Mixin constructors
191 OO.EventEmitter.call( this );
196 actions: 'getAction',
200 this.categorized = {};
203 this.organized = false;
204 this.changing = false;
205 this.changed = false;
207 /* eslint-enable no-unused-vars */
211 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
213 /* Static Properties */
216 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
217 * header of a {@link OO.ui.ProcessDialog process dialog}.
218 * See the [OOUI documentation on MediaWiki][2] for more information and examples.
220 * [2]:https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs
226 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
231 * A 'click' event is emitted when an action is clicked.
233 * @event OO.ui.ActionSet#click
234 * @param {OO.ui.ActionWidget} action Action that was clicked
238 * An 'add' event is emitted when actions are {@link OO.ui.ActionSet#add added} to the action set.
240 * @event OO.ui.ActionSet#add
241 * @param {OO.ui.ActionWidget[]} added Actions added
245 * A 'remove' event is emitted when actions are {@link OO.ui.ActionSet#remove removed}
246 * or {@link OO.ui.ActionSet#clear cleared}.
248 * @event OO.ui.ActionSet#remove
249 * @param {OO.ui.ActionWidget[]} added Actions removed
253 * A 'change' event is emitted when actions are {@link OO.ui.ActionSet#add added}, {@link OO.ui.ActionSet#clear cleared},
254 * or {@link OO.ui.ActionSet#remove removed} from the action set or when the {@link OO.ui.ActionSet#setMode mode}
257 * @event OO.ui.ActionSet#change
263 * Handle action change events.
266 * @fires OO.ui.ActionSet#change
268 OO.ui.ActionSet.prototype.onActionChange = function () {
269 this.organized = false;
270 if ( this.changing ) {
273 this.emit( 'change' );
278 * Check if an action is one of the special actions.
280 * @param {OO.ui.ActionWidget} action Action to check
281 * @return {boolean} Action is special
283 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
284 for ( const flag in this.special ) {
285 if ( action === this.special[ flag ] ) {
294 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
297 * @param {Object} [filters] Filters to use, omit to get all actions
298 * @param {string|string[]} [filters.actions] Actions that action widgets must have
299 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
300 * @param {string|string[]} [filters.modes] Modes that action widgets must have
301 * @param {boolean} [filters.visible] Visibility that action widgets must have, omit to get both
302 * visible and invisible
303 * @param {boolean} [filters.disabled] Disabled state that action widgets must have, omit to get
304 * both enabled and disabled
305 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
307 OO.ui.ActionSet.prototype.get = function ( filters ) {
312 // Collect candidates for the 3 categories "actions", "flags" and "modes"
314 for ( const category in this.categorized ) {
315 let list = filters[ category ];
317 if ( !Array.isArray( list ) ) {
320 for ( i = 0, len = list.length; i < len; i++ ) {
321 const actions = this.categorized[ category ][ list[ i ] ];
322 if ( Array.isArray( actions ) ) {
323 matches.push.apply( matches, actions );
329 // Remove by boolean filters
330 for ( i = 0, len = matches.length; i < len; i++ ) {
331 match = matches[ i ];
333 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
334 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
336 matches.splice( i, 1 );
342 for ( i = 0, len = matches.length; i < len; i++ ) {
343 match = matches[ i ];
344 let index = matches.lastIndexOf( match );
345 while ( index !== i ) {
346 matches.splice( index, 1 );
348 index = matches.lastIndexOf( match );
353 return this.list.slice();
357 * Get 'special' actions.
359 * Special actions are the first visible action widgets with special flags, such as 'safe' and
361 * Special flags can be configured in subclasses by changing the static #specialFlags property.
363 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
365 OO.ui.ActionSet.prototype.getSpecial = function () {
367 return Object.assign( {}, this.special );
371 * Get 'other' actions.
373 * Other actions include all non-special visible action widgets.
375 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
377 OO.ui.ActionSet.prototype.getOthers = function () {
379 return this.others.slice();
383 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
384 * to be available in the specified mode will be made visible. All other actions will be hidden.
386 * @param {string} mode The mode. Only actions configured to be available in the specified
387 * mode will be made visible.
389 * @return {OO.ui.ActionSet} The widget, for chaining
390 * @fires OO.ui.Widget#toggle
391 * @fires OO.ui.ActionSet#change
393 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
394 this.changing = true;
395 for ( let i = 0, len = this.list.length; i < len; i++ ) {
396 const action = this.list[ i ];
397 action.toggle( action.hasMode( mode ) );
400 this.organized = false;
401 this.changing = false;
402 this.emit( 'change' );
408 * Set the abilities of the specified actions.
410 * Action widgets that are configured with the specified actions will be enabled
411 * or disabled based on the boolean values specified in the `actions`
414 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
415 * values that indicate whether or not the action should be enabled.
417 * @return {OO.ui.ActionSet} The widget, for chaining
419 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
420 for ( let i = 0, len = this.list.length; i < len; i++ ) {
421 const item = this.list[ i ];
422 const action = item.getAction();
423 if ( actions[ action ] !== undefined ) {
424 item.setDisabled( !actions[ action ] );
432 * Executes a function once per action.
434 * When making changes to multiple actions, use this method instead of iterating over the actions
435 * manually to defer emitting a #change event until after all actions have been changed.
437 * @param {Object|null} filter Filters to use to determine which actions to iterate over; see #get
438 * @param {Function} callback Callback to run for each action; callback is invoked with three
439 * arguments: the action, the action's index, the list of actions being iterated over
441 * @return {OO.ui.ActionSet} The widget, for chaining
443 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
444 this.changed = false;
445 this.changing = true;
446 this.get( filter ).forEach( callback );
447 this.changing = false;
448 if ( this.changed ) {
449 this.emit( 'change' );
456 * Add action widgets to the action set.
458 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
460 * @return {OO.ui.ActionSet} The widget, for chaining
461 * @fires OO.ui.ActionSet#add
462 * @fires OO.ui.ActionSet#change
464 OO.ui.ActionSet.prototype.add = function ( actions ) {
465 this.changing = true;
466 for ( let i = 0, len = actions.length; i < len; i++ ) {
467 const action = actions[ i ];
468 action.connect( this, {
469 click: [ 'emit', 'click', action ],
470 toggle: [ 'onActionChange' ]
472 this.list.push( action );
474 this.organized = false;
475 this.emit( 'add', actions );
476 this.changing = false;
477 this.emit( 'change' );
483 * Remove action widgets from the set.
485 * To remove all actions, you may wish to use the #clear method instead.
487 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
489 * @return {OO.ui.ActionSet} The widget, for chaining
490 * @fires OO.ui.ActionSet#remove
491 * @fires OO.ui.ActionSet#change
493 OO.ui.ActionSet.prototype.remove = function ( actions ) {
494 this.changing = true;
495 for ( let i = 0, len = actions.length; i < len; i++ ) {
496 const action = actions[ i ];
497 const index = this.list.indexOf( action );
498 if ( index !== -1 ) {
499 action.disconnect( this );
500 this.list.splice( index, 1 );
503 this.organized = false;
504 this.emit( 'remove', actions );
505 this.changing = false;
506 this.emit( 'change' );
512 * Remove all action widgets from the set.
514 * To remove only specified actions, use the {@link OO.ui.ActionSet#remove remove} method instead.
517 * @return {OO.ui.ActionSet} The widget, for chaining
518 * @fires OO.ui.ActionSet#remove
519 * @fires OO.ui.ActionSet#change
521 OO.ui.ActionSet.prototype.clear = function () {
522 const removed = this.list.slice();
524 this.changing = true;
525 for ( let i = 0, len = this.list.length; i < len; i++ ) {
526 const action = this.list[ i ];
527 action.disconnect( this );
532 this.organized = false;
533 this.emit( 'remove', removed );
534 this.changing = false;
535 this.emit( 'change' );
543 * This is called whenever organized information is requested. It will only reorganize the actions
544 * if something has changed since the last time it ran.
548 * @return {OO.ui.ActionSet} The widget, for chaining
550 OO.ui.ActionSet.prototype.organize = function () {
551 const specialFlags = this.constructor.static.specialFlags;
553 if ( !this.organized ) {
554 this.categorized = {};
557 for ( let i = 0, iLen = this.list.length; i < iLen; i++ ) {
558 const action = this.list[ i ];
560 // Populate the 3 categories "actions", "flags" and "modes"
561 for ( const category in this.categories ) {
562 if ( !this.categorized[ category ] ) {
563 this.categorized[ category ] = {};
566 * This calls one of these getters. All return strings or arrays of strings.
567 * {@see OO.ui.ActionWidget.getAction}
568 * {@see OO.ui.FlaggedElement.getFlags}
569 * {@see OO.ui.ActionWidget.getModes}
571 let list = action[ this.categories[ category ] ]();
572 if ( !Array.isArray( list ) ) {
575 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
576 const item = list[ j ];
577 if ( !this.categorized[ category ][ item ] ) {
578 this.categorized[ category ][ item ] = [];
580 this.categorized[ category ][ item ].push( action );
583 if ( action.isVisible() ) {
584 // Populate special/others
586 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
587 const flag = specialFlags[ j ];
588 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
589 this.special[ flag ] = action;
595 this.others.push( action );
599 this.organized = true;
606 * Errors contain a required message (either a string or jQuery selection) that is used to describe
607 * what went wrong in a {@link OO.ui.Process process}. The error's #recoverable and #warning
608 * configurations are used to customize the appearance and functionality of the error interface.
610 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss'
611 * and 'Try again' (i.e., the error is 'recoverable' by default). If the error is not recoverable,
612 * the 'Try again' button will not be rendered and the widget that initiated the failed process will
615 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button,
616 * which will try the process again.
618 * For an example of error interfaces, please see the [OOUI documentation on MediaWiki][1].
620 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Processes_and_errors
625 * @param {string|jQuery} message Description of error
626 * @param {Object} [config] Configuration options
627 * @param {boolean} [config.recoverable=true] Error is recoverable.
628 * By default, errors are recoverable, and users can try the process again.
629 * @param {boolean} [config.warning=false] Error is a warning.
630 * If the error is a warning, the error interface will include a
631 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the
632 * warning is not triggered a second time if the user chooses to continue.
634 OO.ui.Error = function OoUiError( message, config ) {
635 // Allow passing positional parameters inside the config object
636 if ( OO.isPlainObject( message ) && config === undefined ) {
638 message = config.message;
641 // Configuration initialization
642 config = config || {};
645 this.message = message instanceof $ ? message : String( message );
646 this.recoverable = config.recoverable === undefined || !!config.recoverable;
647 this.warning = !!config.warning;
652 OO.initClass( OO.ui.Error );
657 * Check if the error is recoverable.
659 * If the error is recoverable, users are able to try the process again.
661 * @return {boolean} Error is recoverable
663 OO.ui.Error.prototype.isRecoverable = function () {
664 return this.recoverable;
668 * Check if the error is a warning.
670 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
672 * @return {boolean} Error is warning
674 OO.ui.Error.prototype.isWarning = function () {
679 * Get error message as DOM nodes.
681 * @return {jQuery} Error message in DOM nodes
683 OO.ui.Error.prototype.getMessage = function () {
684 return this.message instanceof $ ?
685 this.message.clone() :
686 $( '<div>' ).text( this.message ).contents();
690 * Get the error message text.
692 * @return {string} Error message
694 OO.ui.Error.prototype.getMessageText = function () {
695 return this.message instanceof $ ? this.message.text() : this.message;
699 * A Process is a list of steps that are called in sequence. The step can be a number, a
700 * promise (jQuery, native, or any other “thenable”), or a function:
702 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
703 * - **promise**: the process will continue to the next step when the promise is successfully
704 * resolved or stop if the promise is rejected.
705 * - **function**: the process will execute the function. The process will stop if the function
706 * returns either a boolean `false` or a promise that is rejected; if the function returns a
707 * number, the process will wait for that number of milliseconds before proceeding.
709 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
710 * configured, users can dismiss the error and try the process again, or not. If a process is
711 * stopped, its remaining steps will not be performed.
716 * @param {number|jQuery.Promise|Function} step Number of milliseconds to wait before proceeding,
717 * promise that must be resolved before proceeding, or a function to execute. See #createStep for
718 * more information. See #createStep for more information.
719 * @param {Object} [context=null] Execution context of the function. The context is ignored if the
720 * step is a number or promise.
722 OO.ui.Process = function ( step, context ) {
727 if ( step !== undefined ) {
728 this.next( step, context );
734 OO.initClass( OO.ui.Process );
741 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
742 * If any of the steps return a promise that is rejected or a boolean false, this promise is
743 * rejected and any remaining steps are not performed.
745 OO.ui.Process.prototype.execute = function () {
747 * Continue execution.
750 * @param {Array} step A function and the context it should be called in
751 * @return {Function} Function that continues the process
753 function proceed( step ) {
755 // Execute step in the correct context
756 const result = step.callback.call( step.context );
758 if ( result === false ) {
759 // Use rejected promise for boolean false results
760 return $.Deferred().reject( [] ).promise();
762 if ( typeof result === 'number' ) {
764 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
766 // Use a delayed promise for numbers, expecting them to be in milliseconds
767 const deferred = $.Deferred();
768 setTimeout( deferred.resolve, result );
769 return deferred.promise();
771 if ( result instanceof OO.ui.Error ) {
772 // Use rejected promise for error
773 return $.Deferred().reject( [ result ] ).promise();
775 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
776 // Use rejected promise for list of errors
777 return $.Deferred().reject( result ).promise();
779 // Duck-type the object to see if it can produce a promise
780 if ( result && typeof result.then === 'function' ) {
781 // Use a promise generated from the result
782 return $.when( result ).promise();
784 // Use resolved promise for other results
785 return $.Deferred().resolve().promise();
790 if ( this.steps.length ) {
791 // Generate a chain reaction of promises
792 promise = proceed( this.steps[ 0 ] )();
793 for ( let i = 1, len = this.steps.length; i < len; i++ ) {
794 promise = promise.then( proceed( this.steps[ i ] ) );
797 promise = $.Deferred().resolve().promise();
804 * Create a process step.
807 * @param {number|jQuery.Promise|Function} step
809 * - Number of milliseconds to wait before proceeding
810 * - Promise that must be resolved before proceeding
811 * - Function to execute
812 * - If the function returns a boolean false the process will stop
813 * - If the function returns a promise, the process will continue to the next
814 * step when the promise is resolved or stop if the promise is rejected
815 * - If the function returns a number, the process will wait for that number of
816 * milliseconds before proceeding
817 * @param {Object} [context=null] Execution context of the function. The context is
818 * ignored if the step is a number or promise.
819 * @return {Object} Step object, with `callback` and `context` properties
821 OO.ui.Process.prototype.createStep = function ( step, context ) {
822 if ( typeof step === 'number' || typeof step.then === 'function' ) {
824 callback: function () {
830 if ( typeof step === 'function' ) {
836 throw new Error( 'Cannot create process step: number, promise or function expected' );
840 * Add step to the beginning of the process.
842 * @inheritdoc #createStep
843 * @return {OO.ui.Process} this
846 OO.ui.Process.prototype.first = function ( step, context ) {
847 this.steps.unshift( this.createStep( step, context ) );
852 * Add step to the end of the process.
854 * @inheritdoc #createStep
855 * @return {OO.ui.Process} this
858 OO.ui.Process.prototype.next = function ( step, context ) {
859 this.steps.push( this.createStep( step, context ) );
864 * A window instance represents the life cycle for one single opening of a window
867 * While OO.ui.WindowManager will reuse OO.ui.Window objects, each time a window is
868 * opened, a new lifecycle starts.
870 * For more information, please see the [OOUI documentation on MediaWiki][1].
872 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows
878 OO.ui.WindowInstance = function OoUiWindowInstance() {
880 opening: $.Deferred(),
881 opened: $.Deferred(),
882 closing: $.Deferred(),
890 this.deferreds = deferreds;
892 // Set these up as chained promises so that rejecting of
893 // an earlier stage automatically rejects the subsequent
894 // would-be stages as well.
897 * @property {jQuery.Promise}
899 this.opening = deferreds.opening.promise();
901 * @property {jQuery.Promise}
903 this.opened = this.opening.then( () => deferreds.opened );
905 * @property {jQuery.Promise}
907 this.closing = this.opened.then( () => deferreds.closing );
909 * @property {jQuery.Promise}
911 this.closed = this.closing.then( () => deferreds.closed );
916 OO.initClass( OO.ui.WindowInstance );
919 * Check if window is opening.
921 * @return {boolean} Window is opening
923 OO.ui.WindowInstance.prototype.isOpening = function () {
924 return this.deferreds.opened.state() === 'pending';
928 * Check if window is opened.
930 * @return {boolean} Window is opened
932 OO.ui.WindowInstance.prototype.isOpened = function () {
933 return this.deferreds.opened.state() === 'resolved' &&
934 this.deferreds.closing.state() === 'pending';
938 * Check if window is closing.
940 * @return {boolean} Window is closing
942 OO.ui.WindowInstance.prototype.isClosing = function () {
943 return this.deferreds.closing.state() === 'resolved' &&
944 this.deferreds.closed.state() === 'pending';
948 * Check if window is closed.
950 * @return {boolean} Window is closed
952 OO.ui.WindowInstance.prototype.isClosed = function () {
953 return this.deferreds.closed.state() === 'resolved';
957 * Window managers are used to open and close {@link OO.ui.Window windows} and control their
958 * presentation. Managed windows are mutually exclusive. If a new window is opened while a current
959 * window is opening or is opened, the current window will be closed and any on-going
960 * {@link OO.ui.Process process} will be cancelled. Windows
961 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
962 * pertinent data and reused.
964 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
965 * `opened`, and `closing`, which represent the primary stages of the cycle:
967 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
968 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
970 * - an `opening` event is emitted with an `opening` promise
971 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution
972 * before the window’s {@link OO.ui.Window#method-setup setup} method is called which executes
973 * OO.ui.Window#getSetupProcess.
974 * - a `setup` progress notification is emitted from the `opening` promise
975 * - the #getReadyDelay method is called the returned value is used to time a pause in execution
976 * before the window’s {@link OO.ui.Window#method-ready ready} method is called which executes
977 * OO.ui.Window#getReadyProcess.
978 * - a `ready` progress notification is emitted from the `opening` promise
979 * - the `opening` promise is resolved with an `opened` promise
981 * **Opened**: the window is now open.
983 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
984 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
985 * to close the window.
987 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
988 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution
989 * before the window's {@link OO.ui.Window#getHoldProcess getHoldProcess} method is called on the
990 * window and its result executed
991 * - a `hold` progress notification is emitted from the `closing` promise
992 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in
993 * execution before the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method
994 * is called on the window and its result executed
995 * - a `teardown` progress notification is emitted from the `closing` promise
996 * - the `closing` promise is resolved. The window is now closed
998 * See the [OOUI documentation on MediaWiki][1] for more information.
1000 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
1003 * @extends OO.ui.Element
1004 * @mixes OO.EventEmitter
1007 * @param {Object} [config] Configuration options
1008 * @param {OO.Factory} [config.factory] Window factory to use for automatic instantiation
1009 * Note that window classes that are instantiated with a factory must have
1010 * a {@link OO.ui.Dialog.static.name static name} property that specifies a symbolic name.
1011 * @param {boolean} [config.modal=true] Prevent interaction outside the current window
1012 * @param {boolean} [config.forceTrapFocus] Force the trapping of focus within windows. This is done
1013 * automatically for modal window managers and full screen windows.
1015 OO.ui.WindowManager = function OoUiWindowManager( config ) {
1016 // Configuration initialization
1017 config = config || {};
1019 // Parent constructor
1020 OO.ui.WindowManager.super.call( this, config );
1022 // Mixin constructors
1023 OO.EventEmitter.call( this );
1026 this.factory = config.factory;
1027 this.modal = config.modal === undefined || !!config.modal;
1029 // Deprecated placeholder promise given to compatOpening in openWindow()
1030 // that is resolved in closeWindow().
1031 this.compatOpened = null;
1032 this.preparingToOpen = null;
1033 this.preparingToClose = null;
1034 this.currentWindow = null;
1035 this.lastSize = null;
1036 this.globalEvents = false;
1037 this.$returnFocusTo = null;
1038 this.isolated = false;
1039 this.$ariaHidden = null;
1041 this.onWindowResizeTimeout = null;
1042 this.onWindowResizeHandler = this.onWindowResize.bind( this );
1043 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
1044 this.onWindowFocusHandler = this.onWindowFocus.bind( this );
1048 .addClass( 'oo-ui-windowManager' )
1049 .toggleClass( 'oo-ui-windowManager-modal', this.isModal() )
1050 .toggleClass( 'oo-ui-windowManager-forceTrapFocus', !!config.forceTrapFocus );
1051 if ( this.isModal() ) {
1053 .attr( 'aria-hidden', 'true' )
1054 .attr( 'inert', '' );
1060 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
1061 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
1066 * An 'opening' event is emitted when the window begins to be opened.
1068 * @event OO.ui.WindowManager#opening
1069 * @param {OO.ui.Window} win Window that's being opened
1070 * @param {jQuery.Promise} opened A promise resolved with a value when the window is opened
1071 * successfully. This promise also emits `setup` and `ready` notifications. When this promise is
1072 * resolved, the first argument of the value is an 'closed' promise, the second argument is the
1074 * @param {Object} data Window opening data
1078 * A 'closing' event is emitted when the window begins to be closed.
1080 * @event OO.ui.WindowManager#closing
1081 * @param {OO.ui.Window} win Window that's being closed
1082 * @param {jQuery.Promise} closed A promise resolved with a value when the window is closed
1083 * successfully. This promise also emits `hold` and `teardown` notifications. When this promise is
1084 * resolved, the first argument of its value is the closing data.
1085 * @param {Object} data Window closing data
1089 * A 'resize' event is emitted when a window is resized.
1091 * @event OO.ui.WindowManager#resize
1092 * @param {OO.ui.Window} win Window that was resized
1095 /* Static Properties */
1098 * Map of the symbolic name of each window size and its CSS properties.
1100 * Symbolic name must be valid as a CSS class name suffix.
1103 * @property {Object}
1105 OO.ui.WindowManager.static.sizes = {
1119 // These can be non-numeric because they are never used in calculations
1126 * Symbolic name of the default window size.
1128 * The default size is used if the window's requested size is not recognized.
1131 * @property {string}
1133 OO.ui.WindowManager.static.defaultSize = 'medium';
1138 * Check if the window manager is modal, preventing interaction outside the current window
1140 * @return {boolean} The window manager is modal
1142 OO.ui.WindowManager.prototype.isModal = function () {
1147 * Handle window resize events.
1150 * @param {jQuery.Event} e Window resize event
1152 OO.ui.WindowManager.prototype.onWindowResize = function () {
1153 clearTimeout( this.onWindowResizeTimeout );
1154 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
1158 * Handle window focus events.
1161 * @param {jQuery.Event} e Window focus event
1163 OO.ui.WindowManager.prototype.onWindowFocus = function () {
1164 const currentWindow = this.getCurrentWindow();
1166 // This event should only be bound while a window is open
1168 // Focus can be moved outside the window focus traps but pressing tab
1169 // from the address bar (T307995). When this happens move focus back
1170 // to the start of the current window.
1171 !OO.ui.contains( currentWindow.$element[ 0 ], document.activeElement )
1173 currentWindow.focus();
1178 * Handle window resize events.
1181 * @param {jQuery.Event} e Window resize event
1183 OO.ui.WindowManager.prototype.afterWindowResize = function () {
1184 const currentFocusedElement = document.activeElement;
1185 if ( this.currentWindow ) {
1186 this.updateWindowSize( this.currentWindow );
1188 // Restore focus to the original element if it has changed.
1189 // When a layout change is made on resize inputs lose focus
1190 // on Android (Chrome and Firefox), see T162127.
1191 if ( currentFocusedElement !== document.activeElement ) {
1192 currentFocusedElement.focus();
1198 * Check if window is opening.
1200 * @param {OO.ui.Window} win Window to check
1201 * @return {boolean} Window is opening
1203 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
1204 return win === this.currentWindow && !!this.lifecycle &&
1205 this.lifecycle.isOpening();
1209 * Check if window is closing.
1211 * @param {OO.ui.Window} win Window to check
1212 * @return {boolean} Window is closing
1214 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
1215 return win === this.currentWindow && !!this.lifecycle &&
1216 this.lifecycle.isClosing();
1220 * Check if window is opened.
1222 * @param {OO.ui.Window} win Window to check
1223 * @return {boolean} Window is opened
1225 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
1226 return win === this.currentWindow && !!this.lifecycle &&
1227 this.lifecycle.isOpened();
1231 * Check if a window is being managed.
1233 * @param {OO.ui.Window} win Window to check
1234 * @return {boolean} Window is being managed
1236 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
1237 for ( const name in this.windows ) {
1238 if ( this.windows[ name ] === win ) {
1247 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
1249 * @param {OO.ui.Window} win Window being opened
1250 * @param {Object} [data] Window opening data
1251 * @return {number} Milliseconds to wait
1253 OO.ui.WindowManager.prototype.getSetupDelay = function () {
1258 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’
1261 * @param {OO.ui.Window} win Window being opened
1262 * @param {Object} [data] Window opening data
1263 * @return {number} Milliseconds to wait
1265 OO.ui.WindowManager.prototype.getReadyDelay = function () {
1266 return this.isModal() ? OO.ui.theme.getDialogTransitionDuration() : 0;
1270 * Get the number of milliseconds to wait after closing has begun before executing the 'hold'
1273 * @param {OO.ui.Window} win Window being closed
1274 * @param {Object} [data] Window closing data
1275 * @return {number} Milliseconds to wait
1277 OO.ui.WindowManager.prototype.getHoldDelay = function () {
1282 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
1283 * executing the ‘teardown’ process.
1285 * @param {OO.ui.Window} win Window being closed
1286 * @param {Object} [data] Window closing data
1287 * @return {number} Milliseconds to wait
1289 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
1290 return this.isModal() ? OO.ui.theme.getDialogTransitionDuration() : 0;
1294 * Get a window by its symbolic name.
1296 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will
1297 * be instantiated and added to the window manager automatically. Please see the [OOUI documentation
1298 * on MediaWiki][3] for more information about using factories.
1299 * [3]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
1301 * @param {string} name Symbolic name of the window
1302 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
1303 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
1304 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
1306 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
1307 const deferred = $.Deferred();
1308 let win = this.windows[ name ];
1310 if ( !( win instanceof OO.ui.Window ) ) {
1311 if ( this.factory ) {
1312 if ( !this.factory.lookup( name ) ) {
1313 deferred.reject( new OO.ui.Error(
1314 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
1317 win = this.factory.create( name );
1318 this.addWindows( [ win ] );
1319 deferred.resolve( win );
1322 deferred.reject( new OO.ui.Error(
1323 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
1327 deferred.resolve( win );
1330 return deferred.promise();
1334 * Get current window.
1336 * @return {OO.ui.Window|null} Currently opening/opened/closing window
1338 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
1339 return this.currentWindow;
1345 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
1346 * @param {Object} [data] Window opening data
1347 * @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when
1348 * closed. Defaults the current activeElement. If set to null, focus isn't changed on close.
1349 * @param {OO.ui.WindowInstance} [lifecycle] Used internally
1350 * @param {jQuery.Deferred} [compatOpening] Used internally
1351 * @return {OO.ui.WindowInstance} A lifecycle object representing this particular
1352 * opening of the window. For backwards-compatibility, then object is also a Thenable that is
1353 * resolved when the window is done opening, with nested promise for when closing starts. This
1354 * behaviour is deprecated and is not compatible with jQuery 3, see T163510.
1355 * @fires OO.ui.WindowManager#opening
1357 OO.ui.WindowManager.prototype.openWindow = function ( win, data, lifecycle, compatOpening ) {
1360 // Internal parameter 'lifecycle' allows this method to always return
1361 // a lifecycle even if the window still needs to be created
1362 // asynchronously when 'win' is a string.
1363 lifecycle = lifecycle || new OO.ui.WindowInstance();
1364 compatOpening = compatOpening || $.Deferred();
1366 // Turn lifecycle into a Thenable for backwards-compatibility with
1367 // the deprecated nested-promise behaviour, see T163510.
1368 [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
1369 .forEach( ( method ) => {
1370 lifecycle[ method ] = function () {
1371 OO.ui.warnDeprecation(
1372 'Using the return value of openWindow as a promise is deprecated. ' +
1373 'Use .openWindow( ... ).opening.' + method + '( ... ) instead.'
1375 return compatOpening[ method ].apply( this, arguments );
1379 // Argument handling
1380 if ( typeof win === 'string' ) {
1381 this.getWindow( win ).then(
1383 this.openWindow( w, data, lifecycle, compatOpening );
1386 lifecycle.deferreds.opening.reject( err );
1394 if ( !this.hasWindow( win ) ) {
1395 error = 'Cannot open window: window is not attached to manager';
1396 } else if ( this.lifecycle && this.lifecycle.isOpened() ) {
1397 error = 'Cannot open window: another window is open';
1398 } else if ( this.preparingToOpen || ( this.lifecycle && this.lifecycle.isOpening() ) ) {
1399 error = 'Cannot open window: another window is opening';
1403 compatOpening.reject( new OO.ui.Error( error ) );
1404 lifecycle.deferreds.opening.reject( new OO.ui.Error( error ) );
1408 // If a window is currently closing, wait for it to complete
1409 this.preparingToOpen = $.when( this.lifecycle && this.lifecycle.closed );
1410 // Ensure handlers get called after preparingToOpen is set
1411 this.preparingToOpen.done( () => {
1412 if ( this.isModal() ) {
1413 this.toggleGlobalEvents( true, win );
1414 this.toggleIsolation( true );
1416 this.$returnFocusTo = data.$returnFocusTo !== undefined ?
1417 data.$returnFocusTo :
1418 $( document.activeElement );
1419 this.currentWindow = win;
1420 this.lifecycle = lifecycle;
1421 this.preparingToOpen = null;
1422 this.emit( 'opening', win, compatOpening, data );
1423 lifecycle.deferreds.opening.resolve( data );
1425 this.compatOpened = $.Deferred();
1426 win.setup( data ).then( () => {
1427 compatOpening.notify( { state: 'setup' } );
1429 win.ready( data ).then( () => {
1430 compatOpening.notify( { state: 'ready' } );
1431 lifecycle.deferreds.opened.resolve( data );
1432 compatOpening.resolve( this.compatOpened.promise(), data );
1433 this.togglePreventIosScrolling( true );
1434 }, ( dataOrErr ) => {
1435 lifecycle.deferreds.opened.reject();
1436 compatOpening.reject();
1437 this.closeWindow( win );
1438 if ( dataOrErr instanceof Error ) {
1444 }, this.getReadyDelay() );
1445 }, ( dataOrErr ) => {
1446 lifecycle.deferreds.opened.reject();
1447 compatOpening.reject();
1448 this.closeWindow( win );
1449 if ( dataOrErr instanceof Error ) {
1455 }, this.getSetupDelay() );
1464 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
1465 * @param {Object} [data] Window closing data
1466 * @return {OO.ui.WindowInstance} A lifecycle object representing this particular
1467 * opening of the window. For backwards-compatibility, the object is also a Thenable that is
1468 * resolved when the window is done closing, see T163510.
1469 * @fires OO.ui.WindowManager#closing
1471 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
1472 const compatClosing = $.Deferred();
1473 let lifecycle = this.lifecycle;
1475 // Argument handling
1476 if ( typeof win === 'string' ) {
1477 win = this.windows[ win ];
1478 } else if ( !this.hasWindow( win ) ) {
1485 error = 'Cannot close window: no window is currently open';
1486 } else if ( !win ) {
1487 error = 'Cannot close window: window is not attached to manager';
1488 } else if ( win !== this.currentWindow || this.lifecycle.isClosed() ) {
1489 error = 'Cannot close window: window already closed with different data';
1490 } else if ( this.preparingToClose || this.lifecycle.isClosing() ) {
1491 error = 'Cannot close window: window already closing with different data';
1495 // This function was called for the wrong window and we don't want to mess with the current
1497 lifecycle = new OO.ui.WindowInstance();
1498 // Pretend the window has been opened, so that we can pretend to fail to close it.
1499 lifecycle.deferreds.opening.resolve( {} );
1500 lifecycle.deferreds.opened.resolve( {} );
1503 // Turn lifecycle into a Thenable for backwards-compatibility with
1504 // the deprecated nested-promise behaviour, see T163510.
1505 [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
1506 .forEach( ( method ) => {
1507 lifecycle[ method ] = function () {
1508 OO.ui.warnDeprecation(
1509 'Using the return value of closeWindow as a promise is deprecated. ' +
1510 'Use .closeWindow( ... ).closed.' + method + '( ... ) instead.'
1512 return compatClosing[ method ].apply( this, arguments );
1517 compatClosing.reject( new OO.ui.Error( error ) );
1518 lifecycle.deferreds.closing.reject( new OO.ui.Error( error ) );
1522 // If the window is currently opening, close it when it's done
1523 this.preparingToClose = $.when( this.lifecycle.opened );
1524 // Ensure handlers get called after preparingToClose is set
1525 this.preparingToClose.always( () => {
1526 this.preparingToClose = null;
1527 this.emit( 'closing', win, compatClosing, data );
1528 lifecycle.deferreds.closing.resolve( data );
1529 const compatOpened = this.compatOpened;
1530 this.compatOpened = null;
1531 compatOpened.resolve( compatClosing.promise(), data );
1532 this.togglePreventIosScrolling( false );
1534 win.hold( data ).then( () => {
1535 compatClosing.notify( { state: 'hold' } );
1537 win.teardown( data ).then( () => {
1538 compatClosing.notify( { state: 'teardown' } );
1539 if ( this.isModal() ) {
1540 this.toggleGlobalEvents( false );
1541 this.toggleIsolation( false );
1543 if ( this.$returnFocusTo && this.$returnFocusTo.length ) {
1544 this.$returnFocusTo[ 0 ].focus();
1546 this.currentWindow = null;
1547 this.lifecycle = null;
1548 lifecycle.deferreds.closed.resolve( data );
1549 compatClosing.resolve( data );
1551 }, this.getTeardownDelay() );
1553 }, this.getHoldDelay() );
1560 * Add windows to the window manager.
1562 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
1563 * See the [OOUI documentation on MediaWiki][2] for examples.
1564 * [2]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
1566 * This function can be called in two manners:
1568 * 1. `.addWindows( [ winA, winB, ... ] )` (where `winA`, `winB` are OO.ui.Window objects)
1570 * This syntax registers windows under the symbolic names defined in their `.static.name`
1571 * properties. For example, if `windowA.constructor.static.name` is `'nameA'`, calling
1572 * `.openWindow( 'nameA' )` afterwards will open the window `windowA`. This syntax requires the
1573 * static name to be set, otherwise an exception will be thrown.
1575 * This is the recommended way, as it allows for an easier switch to using a window factory.
1577 * 2. `.addWindows( { nameA: winA, nameB: winB, ... } )`
1579 * This syntax registers windows under the explicitly given symbolic names. In this example,
1580 * calling `.openWindow( 'nameA' )` afterwards will open the window `windowA`, regardless of what
1581 * its `.static.name` is set to. The static name is not required to be set.
1583 * This should only be used if you need to override the default symbolic names.
1587 * const windowManager = new OO.ui.WindowManager();
1588 * $( document.body ).append( windowManager.$element );
1590 * // Add a window under the default name: see OO.ui.MessageDialog.static.name
1591 * windowManager.addWindows( [ new OO.ui.MessageDialog() ] );
1592 * // Add a window under an explicit name
1593 * windowManager.addWindows( { myMessageDialog: new OO.ui.MessageDialog() } );
1595 * // Open window by default name
1596 * windowManager.openWindow( 'message' );
1597 * // Open window by explicitly given name
1598 * windowManager.openWindow( 'myMessageDialog' );
1600 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
1601 * by reference, symbolic name, or explicitly defined symbolic names.
1602 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
1603 * explicit nor a statically configured symbolic name.
1605 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
1607 if ( Array.isArray( windows ) ) {
1608 // Convert to map of windows by looking up symbolic names from static configuration
1610 for ( let i = 0, len = windows.length; i < len; i++ ) {
1611 const name = windows[ i ].constructor.static.name;
1613 throw new Error( 'Windows must have a `name` static property defined.' );
1615 list[ name ] = windows[ i ];
1617 } else if ( OO.isPlainObject( windows ) ) {
1622 for ( const n in list ) {
1623 const win = list[ n ];
1624 this.windows[ n ] = win.toggle( false );
1625 this.$element.append( win.$element );
1626 win.setManager( this );
1631 * Remove the specified windows from the windows manager.
1633 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish
1634 * to use the #clearWindows method instead. If you no longer need the window manager and want to
1635 * ensure that it no longer listens to events, use the #destroy method.
1637 * @param {string[]} names Symbolic names of windows to remove
1638 * @return {jQuery.Promise} Promise resolved when window is closed and removed
1639 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
1641 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
1642 const cleanup = ( name, win ) => {
1643 delete this.windows[ name ];
1644 win.$element.detach();
1647 const promises = names.map( ( name ) => {
1648 const win = this.windows[ name ];
1650 throw new Error( 'Cannot remove window' );
1652 const cleanupWindow = cleanup.bind( null, name, win );
1653 return this.closeWindow( name ).closed.then( cleanupWindow, cleanupWindow );
1656 return $.when.apply( $, promises );
1660 * Remove all windows from the window manager.
1662 * Windows will be closed before they are removed. Note that the window manager, though not in use,
1663 * will still listen to events. If the window manager will not be used again, you may wish to use
1664 * the #destroy method instead. To remove just a subset of windows, use the #removeWindows method.
1666 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
1668 OO.ui.WindowManager.prototype.clearWindows = function () {
1669 return this.removeWindows( Object.keys( this.windows ) );
1673 * Set dialog size. In general, this method should not be called directly.
1675 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
1677 * @param {OO.ui.Window} win Window to update, should be the current window
1679 * @return {OO.ui.WindowManager} The manager, for chaining
1681 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
1682 // Bypass for non-current, and thus invisible, windows
1683 if ( win !== this.currentWindow ) {
1687 const size = win.getSize();
1689 // The following classes are used here
1690 // * oo-ui-windowManager-size-small
1691 // * oo-ui-windowManager-size-medium
1692 // * oo-ui-windowManager-size-large
1693 // * oo-ui-windowManager-size-larger
1694 // * oo-ui-windowManager-size-full
1696 .removeClass( 'oo-ui-windowManager-size-' + this.lastSize )
1697 .addClass( 'oo-ui-windowManager-size-' + size );
1699 this.lastSize = size;
1701 // Backwards compatibility
1702 const isFullscreen = size === 'full';
1703 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
1704 this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
1706 win.setDimensions( win.getSizeProperties() );
1708 this.emit( 'resize', win );
1714 * Prevent scrolling of the document on iOS devices that don't respect `body { overflow: hidden; }`.
1716 * This function is called when the window is opened (ready), and so the background is covered up,
1717 * and the user won't see that we're doing weird things to the scroll position.
1720 * @param {boolean} [on=false]
1722 * @return {OO.ui.WindowManager} The manager, for chaining
1724 OO.ui.WindowManager.prototype.togglePreventIosScrolling = function ( on ) {
1725 const isIos = /ipad|iphone|ipod/i.test( navigator.userAgent ),
1726 $body = $( this.getElementDocument().body ),
1727 stackDepth = ( $body.data( 'windowManagerGlobalEvents' ) || [] ).length;
1729 // Only if this is the first/last WindowManager (see #toggleGlobalEvents)
1730 if ( !isIos || stackDepth !== 1 ) {
1734 const scrollableRoot = OO.ui.Element.static.getRootScrollableElement( $body[ 0 ] );
1737 // We can't apply this workaround for non-fullscreen dialogs, because the user would see the
1738 // scroll position change. If they have content that needs scrolling, you're out of luck…
1739 // Always remember the scroll position in case dialog is closed with different size.
1740 this.iosOrigScrollPosition = scrollableRoot.scrollTop;
1741 if ( this.getCurrentWindow().getSize() === 'full' ) {
1742 $body.add( $body.parent() ).addClass( 'oo-ui-windowManager-ios-modal-ready' );
1745 // Always restore ability to scroll in case dialog was opened with different size.
1746 $body.add( $body.parent() ).removeClass( 'oo-ui-windowManager-ios-modal-ready' );
1747 if ( this.getCurrentWindow().getSize() === 'full' ) {
1748 scrollableRoot.scrollTop = this.iosOrigScrollPosition;
1755 * Bind or unbind global events for scrolling/focus.
1758 * @param {boolean} [on] Bind global events
1759 * @param {OO.ui.Window} [win] The just-opened window (when turning on events)
1761 * @return {OO.ui.WindowManager} The manager, for chaining
1763 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on, win ) {
1764 const $body = $( this.getElementDocument().body );
1765 const $window = $( this.getElementWindow() );
1766 // We could have multiple window managers open so only modify
1767 // the body css at the bottom of the stack
1768 const stack = $body.data( 'windowManagerGlobalEvents' ) || [];
1770 on = on === undefined ? !!this.globalEvents : !!on;
1772 const $bodyAndParent = $body.add( $body.parent() );
1775 if ( !this.globalEvents ) {
1777 // Start listening for top-level window dimension changes
1778 'orientationchange resize': this.onWindowResizeHandler,
1779 focus: this.onWindowFocusHandler
1782 this.globalEvents = true;
1784 } else if ( this.globalEvents ) {
1786 // Stop listening for top-level window dimension changes
1787 'orientationchange resize': this.onWindowResizeHandler,
1788 focus: this.onWindowFocusHandler
1791 this.globalEvents = false;
1794 if ( stack.length > 0 ) {
1795 $bodyAndParent.addClass( 'oo-ui-windowManager-modal-active' );
1796 $bodyAndParent.toggleClass( 'oo-ui-windowManager-modal-active-fullscreen', stack.some( ( w ) => w.getSize() === 'full' ) );
1798 $bodyAndParent.removeClass( 'oo-ui-windowManager-modal-active oo-ui-windowManager-modal-active-fullscreen' );
1800 $body.data( 'windowManagerGlobalEvents', stack );
1806 * Toggle isolation of content other than the window manager.
1808 * This hides the content from screen readers (aria-hidden) and makes
1809 * it invisible to user input events (inert).
1812 * @param {boolean} [isolate] Make only the window manager visible to screen readers
1814 * @return {OO.ui.WindowManager} The manager, for chaining
1816 OO.ui.WindowManager.prototype.toggleIsolation = function ( isolate ) {
1817 this.isolated = isolate === undefined ? !this.isolated : !!isolate;
1819 if ( this.isolated ) {
1820 // In case previously set by another window manager
1822 .removeAttr( 'aria-hidden' )
1823 .removeAttr( 'inert' );
1825 let $el = this.$element;
1827 const ariaHidden = [];
1831 while ( !$el.is( 'body' ) && $el.length ) {
1832 // Hide all siblings at each level, just leaving the path to the manager visible.
1833 const $siblings = $el.siblings().not( 'script' );
1834 // Ensure the path to this manager is visible, as it may have been hidden by
1837 .removeAttr( 'aria-hidden' )
1838 .removeAttr( 'inert' );
1839 // $ariaHidden/$inert exclude elements which already have aria-hidden/inert set,
1840 // as we wouldn't want to reset those attributes when window closes.
1841 // This will also support multiple window managers opening on top of each other,
1842 // as an element hidden by another manager will not be re-enabled until *that*
1843 // manager closes its window.
1844 ariaHidden.push.apply( ariaHidden, $siblings.not( '[aria-hidden=true]' ).toArray() );
1845 inert.push.apply( inert, $siblings.not( '[inert]' ).toArray() );
1848 // Build lists as plain arrays for performance ($.add is slow)
1849 this.$ariaHidden = $( ariaHidden );
1850 this.$inert = $( inert );
1852 // Hide everything other than the window manager from screen readers
1853 this.$ariaHidden.attr( 'aria-hidden', 'true' );
1854 this.$inert.attr( 'inert', '' );
1856 // Restore screen reader visibility
1857 this.$ariaHidden.removeAttr( 'aria-hidden' );
1858 this.$inert.removeAttr( 'inert' );
1859 this.$ariaHidden = null;
1862 // and hide the window manager
1864 .attr( 'aria-hidden', 'true' )
1865 .attr( 'inert', '' );
1872 * Destroy the window manager.
1874 OO.ui.WindowManager.prototype.destroy = function () {
1875 this.clearWindows();
1876 this.$element.remove();
1880 * A window is a container for elements that are in a child frame. They are used with
1881 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1882 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’,
1883 * ‘medium’, ‘large’), which is interpreted by the window manager. If the requested size is not
1884 * recognized, the window manager will choose a sensible fallback.
1886 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1887 * different processes are executed:
1889 * **opening**: The opening stage begins when the window manager's
1890 * {@link OO.ui.WindowManager#openWindow openWindow} or the window's {@link OO.ui.Window#open open} methods are
1891 * used, and the window manager begins to open the window.
1893 * - {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called and its result executed
1894 * - {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called and its result executed
1896 * **opened**: The window is now open
1898 * **closing**: The closing stage begins when the window manager's
1899 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1900 * or the window's {@link OO.ui.Window#close close} methods are used, and the window manager begins to close the
1903 * - {@link OO.ui.Window#getHoldProcess getHoldProcess} method is called and its result executed
1904 * - {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called and its result executed. The window is now closed
1906 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1907 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and
1908 * #getTeardownProcess methods. Note that each {@link OO.ui.Process process} is executed in series,
1909 * so asynchronous processing can complete. Always assume window processes are executed
1912 * For more information, please see the [OOUI documentation on MediaWiki][1].
1914 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows
1918 * @extends OO.ui.Element
1919 * @mixes OO.EventEmitter
1922 * @param {Object} [config] Configuration options
1923 * @param {string} [config.size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1924 * `full`. If omitted, the value of the {@link OO.ui.Window.static.size static size} property will be used.
1926 OO.ui.Window = function OoUiWindow( config ) {
1927 // Configuration initialization
1928 config = config || {};
1930 // Parent constructor
1931 OO.ui.Window.super.call( this, config );
1933 // Mixin constructors
1934 OO.EventEmitter.call( this );
1937 this.manager = null;
1938 this.size = config.size || this.constructor.static.size;
1939 this.$frame = $( '<div>' );
1941 * Overlay element to use for the `$overlay` configuration option of widgets that support it.
1942 * Things put inside it are overlaid on top of the window and are not bound to its dimensions.
1943 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
1945 * MyDialog.prototype.initialize = function () {
1947 * const popupButton = new OO.ui.PopupButtonWidget( {
1948 * $overlay: this.$overlay,
1949 * label: 'Popup button',
1951 * $content: $( '<p>Popup content.</p><p>More content.</p><p>Yet more content.</p>' ),
1958 * @property {jQuery}
1960 this.$overlay = $( '<div>' );
1961 this.$content = $( '<div>' );
1965 * It is considered best practice to trap focus in a loop within a modal dialog, even
1966 * though with 'inert' support we could allow focus to break out to the browser chrome.
1968 * - https://www.w3.org/TR/wai-aria-practices-1.1/examples/dialog-modal/dialog.html#kbd_label
1969 * - https://allyjs.io/tutorials/accessible-dialog.html#reacting-to-kbdtabkbd-and-kbdshift-tabkbd
1970 * - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role#focus_management
1972 this.$focusTrapBefore = $( '<div>' ).addClass( 'oo-ui-window-focusTrap' ).prop( 'tabIndex', 0 );
1973 this.$focusTrapAfter = this.$focusTrapBefore.clone();
1974 this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
1977 this.$overlay.addClass( 'oo-ui-window-overlay' );
1979 .addClass( 'oo-ui-window-content' )
1980 .attr( 'tabindex', -1 );
1982 .addClass( 'oo-ui-window-frame' )
1983 .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
1985 .addClass( 'oo-ui-window' )
1986 .append( this.$frame, this.$overlay );
1988 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1989 // that reference properties not initialized at that time of parent class construction
1990 // TODO: Find a better way to handle post-constructor setup
1991 this.visible = false;
1992 this.$element.addClass( 'oo-ui-element-hidden' );
1997 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1998 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
2000 /* Static Properties */
2003 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
2005 * The static size is used if no #size is configured during construction.
2008 * @property {string}
2010 OO.ui.Window.static.size = 'medium';
2015 * Handle mouse down events.
2018 * @param {jQuery.Event} e Mouse down event
2019 * @return {OO.ui.Window} The window, for chaining
2021 OO.ui.Window.prototype.onMouseDown = function ( e ) {
2022 // Prevent clicking on the click-block from stealing focus
2023 if ( e.target === this.$element[ 0 ] ) {
2029 * Check if the window has been initialized.
2031 * Initialization occurs when a window is added to a manager.
2033 * @return {boolean} Window has been initialized
2035 OO.ui.Window.prototype.isInitialized = function () {
2036 return !!this.manager;
2040 * Check if the window is visible.
2042 * @return {boolean} Window is visible
2044 OO.ui.Window.prototype.isVisible = function () {
2045 return this.visible;
2049 * Check if the window is opening.
2051 * This method is a wrapper around the window manager's
2052 * {@link OO.ui.WindowManager#isOpening isOpening} method.
2054 * @return {boolean} Window is opening
2056 OO.ui.Window.prototype.isOpening = function () {
2057 return this.manager.isOpening( this );
2061 * Check if the window is closing.
2063 * This method is a wrapper around the window manager's
2064 * {@link OO.ui.WindowManager#isClosing isClosing} method.
2066 * @return {boolean} Window is closing
2068 OO.ui.Window.prototype.isClosing = function () {
2069 return this.manager.isClosing( this );
2073 * Check if the window is opened.
2075 * This method is a wrapper around the window manager's
2076 * {@link OO.ui.WindowManager#isOpened isOpened} method.
2078 * @return {boolean} Window is opened
2080 OO.ui.Window.prototype.isOpened = function () {
2081 return this.manager.isOpened( this );
2085 * Get the window manager.
2087 * All windows must be attached to a window manager, which is used to open
2088 * and close the window and control its presentation.
2090 * @return {OO.ui.WindowManager} Manager of window
2092 OO.ui.Window.prototype.getManager = function () {
2093 return this.manager;
2097 * Get the symbolic name of the window size (e.g., `small` or `medium`).
2099 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
2101 OO.ui.Window.prototype.getSize = function () {
2102 const viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() );
2103 const sizes = this.manager.constructor.static.sizes;
2104 let size = this.size;
2106 if ( !sizes[ size ] ) {
2107 size = this.manager.constructor.static.defaultSize;
2109 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
2117 * Get the size properties associated with the current window size
2119 * @return {Object} Size properties
2121 OO.ui.Window.prototype.getSizeProperties = function () {
2122 return this.manager.constructor.static.sizes[ this.getSize() ];
2126 * Disable transitions on window's frame for the duration of the callback function, then enable them
2130 * @param {Function} callback Function to call while transitions are disabled
2132 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
2133 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2134 // Disable transitions first, otherwise we'll get values from when the window was animating.
2135 // We need to build the transition CSS properties using these specific properties since
2136 // Firefox doesn't return anything useful when asked just for 'transition'.
2137 const oldTransition = this.$frame.css( 'transition-property' ) + ' ' +
2138 this.$frame.css( 'transition-duration' ) + ' ' +
2139 this.$frame.css( 'transition-timing-function' ) + ' ' +
2140 this.$frame.css( 'transition-delay' );
2142 this.$frame.css( 'transition', 'none' );
2145 // Force reflow to make sure the style changes done inside callback
2146 // really are not transitioned
2147 this.$frame.height();
2148 this.$frame.css( 'transition', oldTransition );
2152 * Get the height of the full window contents (i.e., the window head, body and foot together).
2154 * What constitutes the head, body, and foot varies depending on the window type.
2155 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
2156 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
2157 * and special actions in the head, and dialog content in the body.
2159 * To get just the height of the dialog body, use the #getBodyHeight method.
2161 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
2163 OO.ui.Window.prototype.getContentHeight = function () {
2164 const body = this.$body[ 0 ];
2165 const frame = this.$frame[ 0 ];
2168 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2169 // Disable transitions first, otherwise we'll get values from when the window was animating.
2170 this.withoutSizeTransitions( () => {
2171 const oldHeight = frame.style.height;
2172 const oldPosition = body.style.position;
2173 const scrollTop = body.scrollTop;
2174 frame.style.height = '1px';
2175 // Force body to resize to new width
2176 body.style.position = 'relative';
2177 bodyHeight = this.getBodyHeight();
2178 frame.style.height = oldHeight;
2179 body.style.position = oldPosition;
2180 body.scrollTop = scrollTop;
2184 // Add buffer for border
2185 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
2186 // Use combined heights of children
2187 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
2192 * Get the height of the window body.
2194 * To get the height of the full window contents (the window body, head, and foot together),
2195 * use #getContentHeight.
2197 * When this function is called, the window will temporarily have been resized
2198 * to height=1px, so .scrollHeight measurements can be taken accurately.
2200 * @return {number} Height of the window body in pixels
2202 OO.ui.Window.prototype.getBodyHeight = function () {
2203 return this.$body[ 0 ].scrollHeight;
2207 * Get the directionality of the frame (right-to-left or left-to-right).
2209 * @return {string} Directionality: `'ltr'` or `'rtl'`
2211 OO.ui.Window.prototype.getDir = function () {
2212 return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2216 * Get the 'setup' process.
2218 * The setup process is used to set up a window for use in a particular context, based on the `data`
2219 * argument. This method is called during the opening phase of the window’s lifecycle (before the
2220 * opening animation). You can add elements to the window in this process or set their default
2223 * Override this method to add additional steps to the ‘setup’ process the parent method provides
2224 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2227 * To add window content that persists between openings, you may wish to use the #initialize method
2230 * @param {Object} [data] Window opening data
2231 * @return {OO.ui.Process} Setup process
2233 OO.ui.Window.prototype.getSetupProcess = function () {
2234 return new OO.ui.Process();
2238 * Get the ‘ready’ process.
2240 * The ready process is used to ready a window for use in a particular context, based on the `data`
2241 * argument. This method is called during the opening phase of the window’s lifecycle, after the
2242 * window has been {@link OO.ui.Window#getSetupProcess setup} (after the opening animation). You can focus
2243 * elements in the window in this process, or open their dropdowns.
2245 * Override this method to add additional steps to the ‘ready’ process the parent method
2246 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2247 * methods of OO.ui.Process.
2249 * @param {Object} [data] Window opening data
2250 * @return {OO.ui.Process} Ready process
2252 OO.ui.Window.prototype.getReadyProcess = function () {
2253 return new OO.ui.Process();
2257 * Get the 'hold' process.
2259 * The hold process is used to keep a window from being used in a particular context, based on the
2260 * `data` argument. This method is called during the closing phase of the window’s lifecycle (before
2261 * the closing animation). You can close dropdowns of elements in the window in this process, if
2262 * they do not get closed automatically.
2264 * Override this method to add additional steps to the 'hold' process the parent method provides
2265 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2268 * @param {Object} [data] Window closing data
2269 * @return {OO.ui.Process} Hold process
2271 OO.ui.Window.prototype.getHoldProcess = function () {
2272 return new OO.ui.Process();
2276 * Get the ‘teardown’ process.
2278 * The teardown process is used to teardown a window after use. During teardown, user interactions
2279 * within the window are conveyed and the window is closed, based on the `data` argument. This
2280 * method is called during the closing phase of the window’s lifecycle (after the closing
2281 * animation). You can remove elements in the window in this process or clear their values.
2283 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2284 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2287 * @param {Object} [data] Window closing data
2288 * @return {OO.ui.Process} Teardown process
2290 OO.ui.Window.prototype.getTeardownProcess = function () {
2291 return new OO.ui.Process();
2295 * Set the window manager.
2297 * This will cause the window to initialize. Calling it more than once will cause an error.
2299 * @param {OO.ui.WindowManager} manager Manager for this window
2300 * @throws {Error} An error is thrown if the method is called more than once
2302 * @return {OO.ui.Window} The window, for chaining
2304 OO.ui.Window.prototype.setManager = function ( manager ) {
2305 if ( this.manager ) {
2306 throw new Error( 'Cannot set window manager, window already has a manager' );
2309 this.manager = manager;
2317 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2319 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2322 * @return {OO.ui.Window} The window, for chaining
2324 OO.ui.Window.prototype.setSize = function ( size ) {
2331 * Update the window size.
2333 * @throws {Error} An error is thrown if the window is not attached to a window manager
2335 * @return {OO.ui.Window} The window, for chaining
2337 OO.ui.Window.prototype.updateSize = function () {
2338 if ( !this.manager ) {
2339 throw new Error( 'Cannot update window size, must be attached to a manager' );
2342 this.manager.updateWindowSize( this );
2348 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2349 * when the window is opening. In general, setDimensions should not be called directly.
2351 * To set the size of the window, use the #setSize method.
2353 * @param {Object} dim CSS dimension properties
2354 * @param {string|number} [dim.width=''] Width
2355 * @param {string|number} [dim.minWidth=''] Minimum width
2356 * @param {string|number} [dim.maxWidth=''] Maximum width
2357 * @param {string|number} [dim.height] Height, omit to set based on height of contents
2358 * @param {string|number} [dim.minHeight=''] Minimum height
2359 * @param {string|number} [dim.maxHeight=''] Maximum height
2361 * @return {OO.ui.Window} The window, for chaining
2363 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2364 const styleObj = this.$frame[ 0 ].style;
2367 // Calculate the height we need to set using the correct width
2368 if ( dim.height === undefined ) {
2369 this.withoutSizeTransitions( () => {
2370 const oldWidth = styleObj.width;
2371 this.$frame.css( 'width', dim.width || '' );
2372 height = this.getContentHeight();
2373 styleObj.width = oldWidth;
2376 height = dim.height;
2380 width: dim.width || '',
2381 minWidth: dim.minWidth || '',
2382 maxWidth: dim.maxWidth || '',
2383 height: height || '',
2384 minHeight: dim.minHeight || '',
2385 maxHeight: dim.maxHeight || ''
2392 * Initialize window contents.
2394 * Before the window is opened for the first time, #initialize is called so that content that
2395 * persists between openings can be added to the window.
2397 * To set up a window with new content each time the window opens, use #getSetupProcess.
2399 * @throws {Error} An error is thrown if the window is not attached to a window manager
2401 * @return {OO.ui.Window} The window, for chaining
2403 OO.ui.Window.prototype.initialize = function () {
2404 if ( !this.manager ) {
2405 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2409 this.$head = $( '<div>' );
2410 this.$body = $( '<div>' );
2411 this.$foot = $( '<div>' );
2412 this.$document = $( this.getElementDocument() );
2415 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2416 this.$focusTraps.on( 'focus', this.onFocusTrapFocused.bind( this ) );
2419 this.$head.addClass( 'oo-ui-window-head' );
2420 this.$body.addClass( 'oo-ui-window-body' );
2421 this.$foot.addClass( 'oo-ui-window-foot' );
2422 this.$content.append( this.$head, this.$body, this.$foot );
2428 * Called when someone tries to focus the hidden element at the end of the dialog.
2429 * Sends focus back to the start of the dialog.
2431 * @param {jQuery.Event} event Focus event
2433 OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
2434 const backwards = this.$focusTrapBefore.is( event.target );
2435 this.focus( backwards );
2441 * @param {boolean} [focusLast=false] Focus the last focusable element in the window, instead of the first
2443 * @return {OO.ui.Window} The window, for chaining
2445 OO.ui.Window.prototype.focus = function ( focusLast ) {
2446 const element = OO.ui.findFocusable( this.$content, !!focusLast );
2448 // There's a focusable element inside the content, at the front or
2449 // back depending on which focus trap we hit; select it.
2452 // There's nothing focusable inside the content. As a fallback,
2453 // this.$content is focusable, and focusing it will keep our focus
2454 // properly trapped. It's not a *meaningful* focus, since it's just
2455 // the content-div for the Window, but it's better than letting focus
2456 // escape into the page.
2457 this.$content.trigger( 'focus' );
2465 * This method is a wrapper around a call to the window
2466 * manager’s {@link OO.ui.WindowManager#openWindow openWindow} method.
2468 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2470 * @param {Object} [data] Window opening data
2471 * @return {OO.ui.WindowInstance} See OO.ui.WindowManager#openWindow
2472 * @throws {Error} An error is thrown if the window is not attached to a window manager
2474 OO.ui.Window.prototype.open = function ( data ) {
2475 if ( !this.manager ) {
2476 throw new Error( 'Cannot open window, must be attached to a manager' );
2479 return this.manager.openWindow( this, data );
2485 * This method is a wrapper around a call to the window
2486 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method.
2488 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2489 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2490 * the window closes.
2492 * @param {Object} [data] Window closing data
2493 * @return {OO.ui.WindowInstance} See OO.ui.WindowManager#closeWindow
2494 * @throws {Error} An error is thrown if the window is not attached to a window manager
2496 OO.ui.Window.prototype.close = function ( data ) {
2497 if ( !this.manager ) {
2498 throw new Error( 'Cannot close window, must be attached to a manager' );
2501 return this.manager.closeWindow( this, data );
2507 * This is called by OO.ui.WindowManager during window opening (before the animation), and should
2508 * not be called directly by other systems.
2510 * @param {Object} [data] Window opening data
2511 * @return {jQuery.Promise} Promise resolved when window is setup
2513 OO.ui.Window.prototype.setup = function ( data ) {
2514 this.toggle( true );
2516 return this.getSetupProcess( data ).execute().then( () => {
2518 // Force redraw by asking the browser to measure the elements' widths
2519 this.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2520 this.$content.addClass( 'oo-ui-window-content-setup' ).width();
2527 * This is called by OO.ui.WindowManager during window opening (after the animation), and should not
2528 * be called directly by other systems.
2530 * @param {Object} [data] Window opening data
2531 * @return {jQuery.Promise} Promise resolved when window is ready
2533 OO.ui.Window.prototype.ready = function ( data ) {
2534 this.$content.trigger( 'focus' );
2535 return this.getReadyProcess( data ).execute().then( () => {
2536 // Force redraw by asking the browser to measure the elements' widths
2537 this.$element.addClass( 'oo-ui-window-ready' ).width();
2538 this.$content.addClass( 'oo-ui-window-content-ready' ).width();
2545 * This is called by OO.ui.WindowManager during window closing (before the animation), and should
2546 * not be called directly by other systems.
2548 * @param {Object} [data] Window closing data
2549 * @return {jQuery.Promise} Promise resolved when window is held
2551 OO.ui.Window.prototype.hold = function ( data ) {
2552 return this.getHoldProcess( data ).execute().then( () => {
2553 // Get the focused element within the window's content
2554 const $focus = this.$content.find(
2555 OO.ui.Element.static.getDocument( this.$content ).activeElement
2558 // Blur the focused element
2559 if ( $focus.length ) {
2563 // Force redraw by asking the browser to measure the elements' widths
2564 this.$element.removeClass( 'oo-ui-window-ready oo-ui-window-setup' ).width();
2565 this.$content.removeClass( 'oo-ui-window-content-ready oo-ui-window-content-setup' ).width();
2572 * This is called by OO.ui.WindowManager during window closing (after the animation), and should not
2573 * be called directly by other systems.
2575 * @param {Object} [data] Window closing data
2576 * @return {jQuery.Promise} Promise resolved when window is torn down
2578 OO.ui.Window.prototype.teardown = function ( data ) {
2579 return this.getTeardownProcess( data ).execute().then( () => {
2580 // Force redraw by asking the browser to measure the elements' widths
2581 this.$element.removeClass( 'oo-ui-window-active' ).width();
2583 this.toggle( false );
2588 * The Dialog class serves as the base class for the other types of dialogs.
2589 * Unless extended to include controls, the rendered dialog box is a simple window
2590 * that users can close by hitting the Escape key. Dialog windows are used with OO.ui.WindowManager,
2591 * which opens, closes, and controls the presentation of the window. See the
2592 * [OOUI documentation on MediaWiki][1] for more information.
2595 * // A simple dialog window.
2596 * function MyDialog( config ) {
2597 * MyDialog.super.call( this, config );
2599 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2600 * MyDialog.static.name = 'myDialog';
2601 * MyDialog.prototype.initialize = function () {
2602 * MyDialog.super.prototype.initialize.call( this );
2603 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2604 * this.content.$element.append( '<p>A simple dialog window. Press Escape key to ' +
2606 * this.$body.append( this.content.$element );
2608 * MyDialog.prototype.getBodyHeight = function () {
2609 * return this.content.$element.outerHeight( true );
2611 * const myDialog = new MyDialog( {
2614 * // Create and append a window manager, which opens and closes the window.
2615 * const windowManager = new OO.ui.WindowManager();
2616 * $( document.body ).append( windowManager.$element );
2617 * windowManager.addWindows( [ myDialog ] );
2618 * // Open the window!
2619 * windowManager.openWindow( myDialog );
2621 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Dialogs
2625 * @extends OO.ui.Window
2626 * @mixes OO.ui.mixin.PendingElement
2629 * @param {Object} [config] Configuration options
2631 OO.ui.Dialog = function OoUiDialog( config ) {
2632 // Parent constructor
2633 OO.ui.Dialog.super.call( this, config );
2635 // Mixin constructors
2636 OO.ui.mixin.PendingElement.call( this );
2639 this.actions = new OO.ui.ActionSet();
2640 this.attachedActions = [];
2641 this.currentAction = null;
2642 this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
2645 this.actions.connect( this, {
2646 click: 'onActionClick',
2647 change: 'onActionsChange'
2652 .addClass( 'oo-ui-dialog' )
2653 .attr( 'role', 'dialog' );
2658 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2659 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2661 /* Static Properties */
2664 * Symbolic name of dialog.
2666 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2667 * Please see the [OOUI documentation on MediaWiki][3] for more information.
2669 * [3]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
2673 * @property {string}
2675 OO.ui.Dialog.static.name = '';
2680 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node,
2681 * or a function that will produce a Label node or string. The title can also be specified with data
2682 * passed to the constructor (see #getSetupProcess). In this case, the static value will be
2687 * @property {jQuery|string|Function}
2689 OO.ui.Dialog.static.title = '';
2692 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2694 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this
2695 * case, the static value will be overridden.
2697 * [2]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
2700 * @property {Object[]}
2702 OO.ui.Dialog.static.actions = [];
2705 * Close the dialog when the Escape key is pressed.
2707 * @deprecated Have #getEscapeAction return `null` instead
2710 * @property {boolean}
2712 OO.ui.Dialog.static.escapable = true;
2717 * The current action to perform if the Escape key is pressed.
2719 * The empty string action closes the dialog (see #getActionProcess).
2720 * The make the escape key do nothing, return `null` here.
2722 * @return {string|null} Action name, or null if unescapable
2724 OO.ui.Dialog.prototype.getEscapeAction = function () {
2729 * Handle frame document key down events.
2732 * @param {jQuery.Event} e Key down event
2734 OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
2735 if ( e.which === OO.ui.Keys.ESCAPE && this.constructor.static.escapable ) {
2736 const action = this.getEscapeAction();
2737 if ( action !== null ) {
2738 this.executeAction( action );
2740 e.stopPropagation();
2742 } else if ( e.which === OO.ui.Keys.ENTER && ( e.ctrlKey || e.metaKey ) ) {
2743 const actions = this.actions.get( { flags: 'primary', visible: true, disabled: false } );
2744 if ( actions.length > 0 ) {
2745 this.executeAction( actions[ 0 ].getAction() );
2747 e.stopPropagation();
2753 * Handle action click events.
2756 * @param {OO.ui.ActionWidget} action Action that was clicked
2758 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2759 if ( !this.isPending() ) {
2760 this.executeAction( action.getAction() );
2765 * Handle actions change event.
2769 OO.ui.Dialog.prototype.onActionsChange = function () {
2770 this.detachActions();
2771 if ( !this.isClosing() ) {
2772 this.attachActions();
2773 if ( !this.isOpening() ) {
2774 // If the dialog is currently opening, this will be called automatically soon.
2781 * Get the set of actions used by the dialog.
2783 * @return {OO.ui.ActionSet}
2785 OO.ui.Dialog.prototype.getActions = function () {
2786 return this.actions;
2790 * Get a process for taking action.
2792 * When you override this method, you can create a new OO.ui.Process and return it, or add
2793 * additional accept steps to the process the parent method provides using the
2794 * {@link OO.ui.Process#first 'first'} and {@link OO.ui.Process#next 'next'} methods of
2797 * @param {string} [action] Symbolic name of action
2798 * @return {OO.ui.Process} Action process
2800 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2801 return new OO.ui.Process()
2804 // An empty action always closes the dialog without data, which should always be
2805 // safe and make no changes
2814 * @param {Object} [data] Dialog opening data
2815 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2816 * the {@link OO.ui.Dialog.static.title static title}
2817 * @param {Object[]} [data.actions] List of configuration options for each
2818 * {@link OO.ui.ActionWidget action widget}, omit to use {@link OO.ui.Dialog.static.actions static actions}.
2820 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2824 return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2826 const config = this.constructor.static,
2827 actions = data.actions !== undefined ? data.actions : config.actions,
2828 title = data.title !== undefined ? data.title : config.title;
2830 this.title.setLabel( title );
2831 this.actions.add( this.getActionWidgets( actions ) );
2833 this.$element.on( 'keydown', this.onDialogKeyDownHandler );
2840 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2842 return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2844 this.$element.off( 'keydown', this.onDialogKeyDownHandler );
2846 this.actions.clear();
2847 this.currentAction = null;
2854 OO.ui.Dialog.prototype.initialize = function () {
2856 OO.ui.Dialog.super.prototype.initialize.call( this );
2859 this.title = new OO.ui.LabelWidget();
2862 this.$content.addClass( 'oo-ui-dialog-content' );
2863 this.$element.attr( 'aria-labelledby', this.title.getElementId() );
2864 this.setPendingElement( this.$head );
2868 * Get action widgets from a list of configs
2870 * @param {Object[]} actions Action widget configs
2871 * @return {OO.ui.ActionWidget[]} Action widgets
2873 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2875 for ( let i = 0, len = actions.length; i < len; i++ ) {
2876 widgets.push( this.getActionWidget( actions[ i ] ) );
2882 * Get action widget from config
2884 * Override this method to change the action widget class used.
2886 * @param {Object} config Action widget config
2887 * @return {OO.ui.ActionWidget} Action widget
2889 OO.ui.Dialog.prototype.getActionWidget = function ( config ) {
2890 return new OO.ui.ActionWidget( this.getActionWidgetConfig( config ) );
2894 * Get action widget config
2896 * Override this method to modify the action widget config
2898 * @param {Object} config Initial action widget config
2899 * @return {Object} Action widget config
2901 OO.ui.Dialog.prototype.getActionWidgetConfig = function ( config ) {
2906 * Attach action actions.
2910 OO.ui.Dialog.prototype.attachActions = function () {
2911 // Remember the list of potentially attached actions
2912 this.attachedActions = this.actions.get();
2916 * Detach action actions.
2920 * @return {OO.ui.Dialog} The dialog, for chaining
2922 OO.ui.Dialog.prototype.detachActions = function () {
2923 // Detach all actions that may have been previously attached
2924 for ( let i = 0, len = this.attachedActions.length; i < len; i++ ) {
2925 this.attachedActions[ i ].$element.detach();
2927 this.attachedActions = [];
2933 * Execute an action.
2935 * @param {string} action Symbolic name of action to execute
2936 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2938 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2939 const actionWidgets = this.actions.get( { actions: [ action ], visible: true } );
2940 // If the action is shown as an ActionWidget, but is disabled, then do nothing.
2941 if ( actionWidgets.length && actionWidgets.every( ( widget ) => widget.isDisabled() ) ) {
2942 return $.Deferred().reject().promise();
2945 this.currentAction = action;
2946 return this.getActionProcess( action ).execute()
2947 .always( this.popPending.bind( this ) );
2951 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
2952 * consists of a header that contains the dialog title, a body with the message, and a footer that
2953 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
2954 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
2956 * There are two basic types of message dialogs, confirmation and alert:
2958 * - **confirmation**: the dialog title describes what a progressive action will do and the message
2959 * provides more details about the consequences.
2960 * - **alert**: the dialog title describes which event occurred and the message provides more
2961 * information about why the event occurred.
2963 * The MessageDialog class specifies two actions: ‘accept’, the primary
2964 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
2965 * passing along the selected action.
2967 * For more information and examples, please see the [OOUI documentation on MediaWiki][1].
2970 * // Example: Creating and opening a message dialog window.
2971 * const messageDialog = new OO.ui.MessageDialog();
2973 * // Create and append a window manager.
2974 * const windowManager = new OO.ui.WindowManager();
2975 * $( document.body ).append( windowManager.$element );
2976 * windowManager.addWindows( [ messageDialog ] );
2977 * // Open the window.
2978 * windowManager.openWindow( messageDialog, {
2979 * title: 'Basic message dialog',
2980 * message: 'This is the message'
2983 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
2986 * @extends OO.ui.Dialog
2989 * @param {Object} [config] Configuration options
2991 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
2992 // Parent constructor
2993 OO.ui.MessageDialog.super.call( this, config );
2996 this.verticalActionLayout = null;
2999 this.$element.addClass( 'oo-ui-messageDialog' );
3004 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
3006 /* Static Properties */
3012 OO.ui.MessageDialog.static.name = 'message';
3018 OO.ui.MessageDialog.static.size = 'small';
3023 * The title of a confirmation dialog describes what a progressive action will do. The
3024 * title of an alert dialog describes which event occurred.
3027 * @property {jQuery|string|Function|null}
3029 OO.ui.MessageDialog.static.title = null;
3032 * The message displayed in the dialog body.
3034 * A confirmation message describes the consequences of a progressive action. An alert
3035 * message describes why an event occurred.
3038 * @property {jQuery|string|Function|null}
3040 OO.ui.MessageDialog.static.message = null;
3046 OO.ui.MessageDialog.static.actions = [
3047 // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
3048 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
3049 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
3055 * Toggle action layout between vertical and horizontal.
3058 * @param {boolean} [value] Layout actions vertically, omit to toggle
3060 * @return {OO.ui.MessageDialog} The dialog, for chaining
3062 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
3063 value = value === undefined ? !this.verticalActionLayout : !!value;
3065 if ( value !== this.verticalActionLayout ) {
3066 this.verticalActionLayout = value;
3068 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
3069 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
3078 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
3080 return new OO.ui.Process( () => {
3081 this.close( { action: action } );
3084 return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
3090 * @param {Object} [data] Dialog opening data
3091 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
3092 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
3093 * @param {string} [data.size] Symbolic name of the dialog size, see {@link OO.ui.Window}
3094 * @param {Object[]} [data.actions] List of {@link OO.ui.ActionOptionWidget} configuration options for each
3097 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
3101 return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
3103 this.title.setLabel(
3104 data.title !== undefined ? data.title : this.constructor.static.title
3106 this.message.setLabel(
3107 data.message !== undefined ? data.message : this.constructor.static.message
3109 this.size = data.size !== undefined ? data.size : this.constructor.static.size;
3116 OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
3120 return OO.ui.MessageDialog.super.prototype.getReadyProcess.call( this, data )
3122 // Focus the primary action button
3123 let actions = this.actions.get();
3124 actions = actions.filter( ( action ) => action.getFlags().indexOf( 'primary' ) > -1 );
3125 if ( actions.length > 0 ) {
3126 actions[ 0 ].focus();
3134 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
3135 const $scrollable = this.container.$element;
3137 const oldOverflow = $scrollable[ 0 ].style.overflow;
3138 $scrollable[ 0 ].style.overflow = 'hidden';
3140 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
3142 const bodyHeight = this.text.$element.outerHeight( true );
3143 $scrollable[ 0 ].style.overflow = oldOverflow;
3151 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
3152 const $scrollable = this.container.$element;
3155 OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
3157 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
3158 // Need to do it after transition completes (250ms), add 50ms just in case.
3160 const oldOverflow = $scrollable[ 0 ].style.overflow,
3161 activeElement = document.activeElement;
3163 $scrollable[ 0 ].style.overflow = 'hidden';
3165 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
3167 // Check reconsiderScrollbars didn't destroy our focus, as we
3168 // are doing this after the ready process.
3169 if ( activeElement && activeElement !== document.activeElement && activeElement.focus ) {
3170 activeElement.focus();
3173 $scrollable[ 0 ].style.overflow = oldOverflow;
3177 // Wait for CSS transition to finish and do it again :(
3188 OO.ui.MessageDialog.prototype.initialize = function () {
3190 OO.ui.MessageDialog.super.prototype.initialize.call( this );
3193 this.$actions = $( '<div>' );
3194 this.container = new OO.ui.PanelLayout( {
3195 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
3197 this.text = new OO.ui.PanelLayout( {
3198 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
3200 this.message = new OO.ui.LabelWidget( {
3201 classes: [ 'oo-ui-messageDialog-message' ]
3205 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
3206 this.$content.addClass( 'oo-ui-messageDialog-content' );
3207 this.container.$element.append( this.text.$element );
3208 this.text.$element.append( this.title.$element, this.message.$element );
3209 this.$body.append( this.container.$element );
3210 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
3211 this.$foot.append( this.$actions );
3217 OO.ui.MessageDialog.prototype.getActionWidgetConfig = function ( config ) {
3219 return Object.assign( {}, config, { framed: false } );
3225 OO.ui.MessageDialog.prototype.attachActions = function () {
3227 OO.ui.MessageDialog.super.prototype.attachActions.call( this );
3229 const special = this.actions.getSpecial();
3230 const others = this.actions.getOthers();
3232 if ( special.safe ) {
3233 this.$actions.append( special.safe.$element );
3234 special.safe.toggleFramed( true );
3236 for ( let i = 0, len = others.length; i < len; i++ ) {
3237 this.$actions.append( others[ i ].$element );
3238 others[ i ].toggleFramed( true );
3240 if ( special.primary ) {
3241 this.$actions.append( special.primary.$element );
3242 special.primary.toggleFramed( true );
3247 * Fit action actions into columns or rows.
3249 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
3253 OO.ui.MessageDialog.prototype.fitActions = function () {
3254 const previous = this.verticalActionLayout;
3257 this.toggleVerticalActionLayout( false );
3258 if ( this.$actions[ 0 ].scrollWidth > this.$actions[ 0 ].clientWidth ) {
3259 this.toggleVerticalActionLayout( true );
3262 // Move the body out of the way of the foot
3263 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
3265 if ( this.verticalActionLayout !== previous ) {
3266 // We changed the layout, window height might need to be updated.
3272 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
3273 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
3274 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again
3275 * when relevant. The ProcessDialog class is always extended and customized with the actions and
3276 * content required for each process.
3278 * The process dialog box consists of a header that visually represents the ‘working’ state of long
3279 * processes with an animation. The header contains the dialog title as well as
3280 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
3281 * a ‘primary’ action on the right (e.g., ‘Done’).
3283 * Like other windows, the process dialog is managed by a
3284 * {@link OO.ui.WindowManager window manager}.
3285 * Please see the [OOUI documentation on MediaWiki][1] for more information and examples.
3288 * // Example: Creating and opening a process dialog window.
3289 * function MyProcessDialog( config ) {
3290 * MyProcessDialog.super.call( this, config );
3292 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
3294 * MyProcessDialog.static.name = 'myProcessDialog';
3295 * MyProcessDialog.static.title = 'Process dialog';
3296 * MyProcessDialog.static.actions = [
3297 * { action: 'save', label: 'Done', flags: 'primary' },
3298 * { label: 'Cancel', flags: 'safe' }
3301 * MyProcessDialog.prototype.initialize = function () {
3302 * MyProcessDialog.super.prototype.initialize.apply( this, arguments );
3303 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
3304 * this.content.$element.append( '<p>This is a process dialog window. The header ' +
3305 * 'contains the title and two buttons: \'Cancel\' (a safe action) on the left and ' +
3306 * '\'Done\' (a primary action) on the right.</p>' );
3307 * this.$body.append( this.content.$element );
3309 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
3311 * return new OO.ui.Process( () => {
3312 * this.close( { action: action } );
3315 * return MyProcessDialog.super.prototype.getActionProcess.call( this, action );
3318 * const windowManager = new OO.ui.WindowManager();
3319 * $( document.body ).append( windowManager.$element );
3321 * const dialog = new MyProcessDialog();
3322 * windowManager.addWindows( [ dialog ] );
3323 * windowManager.openWindow( dialog );
3325 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs
3329 * @extends OO.ui.Dialog
3332 * @param {Object} [config] Configuration options
3334 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
3335 // Parent constructor
3336 OO.ui.ProcessDialog.super.call( this, config );
3339 this.fitOnOpen = false;
3342 this.$element.addClass( 'oo-ui-processDialog' );
3343 if ( OO.ui.isMobile() ) {
3344 this.$element.addClass( 'oo-ui-isMobile' );
3350 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
3355 * Handle dismiss button click events.
3361 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
3366 * Handle retry button click events.
3368 * Hides errors and then tries again.
3372 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
3374 this.executeAction( this.currentAction );
3380 OO.ui.ProcessDialog.prototype.initialize = function () {
3382 OO.ui.ProcessDialog.super.prototype.initialize.call( this );
3385 this.$navigation = $( '<div>' );
3386 this.$location = $( '<div>' );
3387 this.$safeActions = $( '<div>' );
3388 this.$primaryActions = $( '<div>' );
3389 this.$otherActions = $( '<div>' );
3390 this.dismissButton = new OO.ui.ButtonWidget( {
3391 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
3393 this.retryButton = new OO.ui.ButtonWidget();
3394 this.$errors = $( '<div>' );
3395 this.$errorsTitle = $( '<div>' );
3398 this.dismissButton.connect( this, {
3399 click: 'onDismissErrorButtonClick'
3401 this.retryButton.connect( this, {
3402 click: 'onRetryButtonClick'
3404 this.title.connect( this, {
3405 labelChange: 'fitLabel'
3409 this.title.$element.addClass( 'oo-ui-processDialog-title' );
3411 .append( this.title.$element )
3412 .addClass( 'oo-ui-processDialog-location' );
3413 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
3414 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
3415 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
3417 .addClass( 'oo-ui-processDialog-errors-title' )
3418 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
3420 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
3423 $( '<div>' ).addClass( 'oo-ui-processDialog-errors-actions' ).append(
3424 this.dismissButton.$element, this.retryButton.$element
3428 .addClass( 'oo-ui-processDialog-content' )
3429 .append( this.$errors );
3431 .addClass( 'oo-ui-processDialog-navigation' )
3432 // Note: Order of appends below is important. These are in the order
3433 // we want tab to go through them. Display-order is handled entirely
3434 // by CSS absolute-positioning. As such, primary actions like "done"
3436 .append( this.$primaryActions, this.$location, this.$safeActions );
3437 this.$head.append( this.$navigation );
3438 this.$foot.append( this.$otherActions );
3444 OO.ui.ProcessDialog.prototype.getActionWidgetConfig = function ( config ) {
3445 function checkFlag( flag ) {
3446 return config.flags === flag ||
3447 ( Array.isArray( config.flags ) && config.flags.indexOf( flag ) !== -1 );
3450 config = Object.assign( { framed: true }, config );
3451 if ( checkFlag( 'close' ) ) {
3452 // Change close buttons to icon only.
3453 Object.assign( config, {
3455 invisibleLabel: true
3457 } else if ( checkFlag( 'back' ) ) {
3458 // Change back buttons to icon only.
3459 Object.assign( config, {
3461 invisibleLabel: true
3471 OO.ui.ProcessDialog.prototype.attachActions = function () {
3473 OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
3475 const special = this.actions.getSpecial();
3476 const others = this.actions.getOthers();
3477 if ( special.primary ) {
3478 this.$primaryActions.append( special.primary.$element );
3480 for ( let i = 0, len = others.length; i < len; i++ ) {
3481 const other = others[ i ];
3482 this.$otherActions.append( other.$element );
3484 if ( special.safe ) {
3485 this.$safeActions.append( special.safe.$element );
3492 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
3493 return OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
3494 .fail( ( errors ) => {
3495 this.showErrors( errors || [] );
3502 OO.ui.ProcessDialog.prototype.setDimensions = function () {
3504 OO.ui.ProcessDialog.super.prototype.setDimensions.apply( this, arguments );
3508 // If there are many actions, they might be shown on multiple lines. Their layout can change
3509 // when resizing the dialog and when changing the actions. Adjust the height of the footer to
3511 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
3512 // Wait for CSS transition to finish and do it again :(
3514 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
3519 * Fit label between actions.
3523 * @return {OO.ui.MessageDialog} The dialog, for chaining
3525 OO.ui.ProcessDialog.prototype.fitLabel = function () {
3526 const size = this.getSizeProperties();
3528 let navigationWidth;
3529 if ( typeof size.width !== 'number' ) {
3530 if ( this.isOpened() ) {
3531 navigationWidth = this.$head.width() - 20;
3532 } else if ( this.isOpening() ) {
3533 if ( !this.fitOnOpen ) {
3534 // Size is relative and the dialog isn't open yet, so wait.
3535 // FIXME: This should ideally be handled by setup somehow.
3536 this.manager.lifecycle.opened.done( this.fitLabel.bind( this ) );
3537 this.fitOnOpen = true;
3544 navigationWidth = size.width - 20;
3547 const safeWidth = this.$safeActions.width();
3548 const primaryWidth = this.$primaryActions.width();
3549 const biggerWidth = Math.max( safeWidth, primaryWidth );
3551 const labelWidth = this.title.$element.width();
3553 let leftWidth, rightWidth;
3554 if ( !OO.ui.isMobile() && 2 * biggerWidth + labelWidth < navigationWidth ) {
3555 // We have enough space to center the label
3556 leftWidth = rightWidth = biggerWidth;
3558 // Let's hope we at least have enough space not to overlap, because we can't wrap
3560 if ( this.getDir() === 'ltr' ) {
3561 leftWidth = safeWidth;
3562 rightWidth = primaryWidth;
3564 leftWidth = primaryWidth;
3565 rightWidth = safeWidth;
3569 this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
3575 * Handle errors that occurred during accept or reject processes.
3578 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
3580 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
3583 let recoverable = true,
3586 if ( errors instanceof OO.ui.Error ) {
3587 errors = [ errors ];
3590 for ( let i = 0, len = errors.length; i < len; i++ ) {
3591 if ( !errors[ i ].isRecoverable() ) {
3592 recoverable = false;
3594 if ( errors[ i ].isWarning() ) {
3597 items.push( new OO.ui.MessageWidget( {
3599 label: errors[ i ].getMessage()
3600 } ).$element[ 0 ] );
3602 this.$errorItems = $( items );
3603 if ( recoverable ) {
3604 abilities[ this.currentAction ] = true;
3605 // Copy the flags from the first matching action.
3606 const actions = this.actions.get( { actions: this.currentAction } );
3607 if ( actions.length ) {
3608 this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
3611 abilities[ this.currentAction ] = false;
3612 this.actions.setAbilities( abilities );
3615 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
3617 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
3619 this.retryButton.toggle( recoverable );
3620 this.$errorsTitle.after( this.$errorItems );
3621 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
3629 OO.ui.ProcessDialog.prototype.hideErrors = function () {
3630 this.$errors.addClass( 'oo-ui-element-hidden' );
3631 if ( this.$errorItems ) {
3632 this.$errorItems.remove();
3633 this.$errorItems = null;
3640 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
3642 return OO.ui.ProcessDialog.super.prototype.getTeardownProcess.call( this, data )
3644 // Make sure to hide errors.
3646 this.fitOnOpen = false;
3651 * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
3655 * @return {OO.ui.WindowManager}
3657 OO.ui.getWindowManager = function () {
3658 if ( !OO.ui.windowManager ) {
3659 OO.ui.windowManager = new OO.ui.WindowManager();
3660 $( OO.ui.getTeleportTarget() ).append( OO.ui.windowManager.$element );
3661 OO.ui.windowManager.addWindows( [ new OO.ui.MessageDialog() ] );
3663 return OO.ui.windowManager;
3667 * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
3668 * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
3669 * has only one action button, labelled "OK", clicking it will simply close the dialog.
3671 * A window manager is created automatically when this function is called for the first time.
3674 * OO.ui.alert( 'Something happened!' ).done( function () {
3675 * console.log( 'User closed the dialog.' );
3678 * OO.ui.alert( 'Something larger happened!', { size: 'large' } );
3680 * @param {jQuery|string|Function} text Message text to display
3681 * @param {Object} [options] Additional options, see {@link OO.ui.MessageDialog#getSetupProcess}
3682 * @return {jQuery.Promise} Promise resolved when the user closes the dialog
3684 OO.ui.alert = function ( text, options ) {
3685 return OO.ui.getWindowManager().openWindow( 'message', Object.assign( {
3687 actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
3688 }, options ) ).closed.then( () => undefined );
3692 * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
3693 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3694 * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
3695 * (labelled "Cancel").
3697 * A window manager is created automatically when this function is called for the first time.
3700 * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
3701 * if ( confirmed ) {
3702 * console.log( 'User clicked "OK"!' );
3704 * console.log( 'User clicked "Cancel" or closed the dialog.' );
3708 * @param {jQuery|string|Function} text Message text to display
3709 * @param {Object} [options] Additional options, see {@link OO.ui.MessageDialog#getSetupProcess}
3710 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3711 * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
3714 OO.ui.confirm = function ( text, options ) {
3715 return OO.ui.getWindowManager().openWindow( 'message', Object.assign( {
3717 }, options ) ).closed.then( ( data ) => !!( data && data.action === 'accept' ) );
3721 * Display a quick modal prompt dialog, using a OO.ui.MessageDialog. While the dialog is open,
3722 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3723 * dialog has a text input widget and two action buttons, one to confirm an operation
3724 * (labelled "OK") and one to cancel it (labelled "Cancel").
3726 * A window manager is created automatically when this function is called for the first time.
3729 * OO.ui.prompt( 'Choose a line to go to', {
3730 * textInput: { placeholder: 'Line number' }
3731 * } ).done( function ( result ) {
3732 * if ( result !== null ) {
3733 * console.log( 'User typed "' + result + '" then clicked "OK".' );
3735 * console.log( 'User clicked "Cancel" or closed the dialog.' );
3739 * @param {jQuery|string|Function} text Message text to display
3740 * @param {Object} [options] Additional options, see {@link OO.ui.MessageDialog#getSetupProcess}
3741 * @param {Object} [options.textInput] Additional options for text input widget,
3742 * see {@link OO.ui.TextInputWidget}
3743 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3744 * confirm, the promise will resolve with the value of the text input widget; otherwise, it will
3745 * resolve to `null`.
3747 OO.ui.prompt = function ( text, options ) {
3748 const manager = OO.ui.getWindowManager(),
3749 textInput = new OO.ui.TextInputWidget( ( options && options.textInput ) || {} ),
3750 textField = new OO.ui.FieldLayout( textInput, {
3755 const instance = manager.openWindow( 'message', Object.assign( {
3756 message: textField.$element
3759 // TODO: This is a little hacky, and could be done by extending MessageDialog instead.
3760 instance.opened.then( () => {
3761 textInput.on( 'enter', () => {
3762 manager.getCurrentWindow().close( { action: 'accept' } );
3767 return instance.closed.then( ( data ) => data && data.action === 'accept' ? textInput.getValue() : null );
3772 //# sourceMappingURL=oojs-ui-windows.js.map.json