Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / lib / ooui / oojs-ui-windows.js
blob6fa1e8c2957b7a84421922f58fddfde49b0817b3
1 /*!
2  * OOUI v0.51.4
3  * https://www.mediawiki.org/wiki/OOUI
4  *
5  * Copyright 2011–2024 OOUI Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2024-12-05T17:34:41Z
10  */
11 ( function ( OO ) {
13 'use strict';
15 /**
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
18  * of the actions.
19  *
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
22  * and examples.
23  *
24  * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
25  *
26  * @class
27  * @extends OO.ui.ButtonWidget
28  * @mixes OO.ui.mixin.PendingElement
29  *
30  * @constructor
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
37  */
38 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
39         // Configuration initialization
40         config = Object.assign( { framed: false }, config );
42         // Parent constructor
43         OO.ui.ActionWidget.super.call( this, config );
45         // Mixin constructors
46         OO.ui.mixin.PendingElement.call( this, config );
48         // Properties
49         this.action = config.action || '';
50         this.modes = config.modes || [];
51         this.width = 0;
52         this.height = 0;
54         // Initialization
55         this.$element.addClass( 'oo-ui-actionWidget' );
58 /* Setup */
60 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
61 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
63 /* Methods */
65 /**
66  * Check if the action is configured to be available in the specified `mode`.
67  *
68  * @param {string} mode Name of mode
69  * @return {boolean} The action is configured with the mode
70  */
71 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
72         return this.modes.indexOf( mode ) !== -1;
75 /**
76  * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
77  *
78  * @return {string}
79  */
80 OO.ui.ActionWidget.prototype.getAction = function () {
81         return this.action;
84 /**
85  * Get the symbolic name of the mode or modes for which the action is configured to be available.
86  *
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.
90  *
91  * @return {string[]}
92  */
93 OO.ui.ActionWidget.prototype.getModes = function () {
94         return this.modes.slice();
97 /* eslint-disable no-unused-vars */
98 /**
99  * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that
100  * comprise them.
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
115  *     @example
116  *     // Example: An action set used in a process dialog
117  *     function MyProcessDialog( config ) {
118  *         MyProcessDialog.super.call( this, config );
119  *     }
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 = [
125  *         {
126  *           action: 'continue',
127  *           modes: 'edit',
128  *           label: 'Continue',
129  *           flags: [ 'primary', 'progressive' ]
130  *         },
131  *         { action: 'help', modes: 'edit', label: 'Help' },
132  *         { modes: 'edit', label: 'Cancel', flags: 'safe' },
133  *         { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
134  *     ];
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.' +
145  *              '</p>' );
146  *         this.stackLayout = new OO.ui.StackLayout( {
147  *             items: [ this.panel1, this.panel2 ]
148  *         } );
149  *         this.$body.append( this.stackLayout.$element );
150  *     };
151  *     MyProcessDialog.prototype.getSetupProcess = function ( data ) {
152  *         return MyProcessDialog.super.prototype.getSetupProcess.call( this, data )
153  *             .next( () => {
154  *                 this.actions.setMode( 'edit' );
155  *             } );
156  *     };
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( () => {
166  *                 this.close();
167  *             } );
168  *         }
169  *         return MyProcessDialog.super.prototype.getActionProcess.call( this, action );
170  *     };
171  *     MyProcessDialog.prototype.getBodyHeight = function () {
172  *         return this.panel1.$element.outerHeight( true );
173  *     };
174  *     const windowManager = new OO.ui.WindowManager();
175  *     $( document.body ).append( windowManager.$element );
176  *     const dialog = new MyProcessDialog( {
177  *         size: 'medium'
178  *     } );
179  *     windowManager.addWindows( [ dialog ] );
180  *     windowManager.openWindow( dialog );
182  * @abstract
183  * @class
184  * @mixes OO.EventEmitter
186  * @constructor
187  * @param {Object} [config] Configuration options
188  */
189 OO.ui.ActionSet = function OoUiActionSet( config ) {
190         // Mixin constructors
191         OO.EventEmitter.call( this );
193         // Properties
194         this.list = [];
195         this.categories = {
196                 actions: 'getAction',
197                 flags: 'getFlags',
198                 modes: 'getModes'
199         };
200         this.categorized = {};
201         this.special = {};
202         this.others = [];
203         this.organized = false;
204         this.changing = false;
205         this.changed = false;
207 /* eslint-enable no-unused-vars */
209 /* Setup */
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
222  * @abstract
223  * @static
224  * @property {string}
225  */
226 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
228 /* Events */
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
235  */
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
242  */
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
250  */
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}
255  * is changed.
257  * @event OO.ui.ActionSet#change
258  */
260 /* Methods */
263  * Handle action change events.
265  * @private
266  * @fires OO.ui.ActionSet#change
267  */
268 OO.ui.ActionSet.prototype.onActionChange = function () {
269         this.organized = false;
270         if ( this.changing ) {
271                 this.changed = true;
272         } else {
273                 this.emit( 'change' );
274         }
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
282  */
283 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
284         for ( const flag in this.special ) {
285                 if ( action === this.special[ flag ] ) {
286                         return true;
287                 }
288         }
290         return false;
294  * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
295  *  or ‘disabled’.
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
306  */
307 OO.ui.ActionSet.prototype.get = function ( filters ) {
308         if ( filters ) {
309                 this.organize();
311                 let i, len;
312                 // Collect candidates for the 3 categories "actions", "flags" and "modes"
313                 const matches = [];
314                 for ( const category in this.categorized ) {
315                         let list = filters[ category ];
316                         if ( list ) {
317                                 if ( !Array.isArray( list ) ) {
318                                         list = [ list ];
319                                 }
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 );
324                                         }
325                                 }
326                         }
327                 }
328                 let match;
329                 // Remove by boolean filters
330                 for ( i = 0, len = matches.length; i < len; i++ ) {
331                         match = matches[ i ];
332                         if (
333                                 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
334                                 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
335                         ) {
336                                 matches.splice( i, 1 );
337                                 len--;
338                                 i--;
339                         }
340                 }
341                 // Remove duplicates
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 );
347                                 len--;
348                                 index = matches.lastIndexOf( match );
349                         }
350                 }
351                 return matches;
352         }
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
360  * 'primary'.
361  * Special flags can be configured in subclasses by changing the static #specialFlags property.
363  * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
364  */
365 OO.ui.ActionSet.prototype.getSpecial = function () {
366         this.organize();
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
376  */
377 OO.ui.ActionSet.prototype.getOthers = function () {
378         this.organize();
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.
388  * @chainable
389  * @return {OO.ui.ActionSet} The widget, for chaining
390  * @fires OO.ui.Widget#toggle
391  * @fires OO.ui.ActionSet#change
392  */
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 ) );
398         }
400         this.organized = false;
401         this.changing = false;
402         this.emit( 'change' );
404         return this;
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`
412  * parameter.
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.
416  * @chainable
417  * @return {OO.ui.ActionSet} The widget, for chaining
418  */
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 ] );
425                 }
426         }
428         return this;
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
440  * @chainable
441  * @return {OO.ui.ActionSet} The widget, for chaining
442  */
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' );
450         }
452         return this;
456  * Add action widgets to the action set.
458  * @param {OO.ui.ActionWidget[]} actions Action widgets to add
459  * @chainable
460  * @return {OO.ui.ActionSet} The widget, for chaining
461  * @fires OO.ui.ActionSet#add
462  * @fires OO.ui.ActionSet#change
463  */
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' ]
471                 } );
472                 this.list.push( action );
473         }
474         this.organized = false;
475         this.emit( 'add', actions );
476         this.changing = false;
477         this.emit( 'change' );
479         return this;
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
488  * @chainable
489  * @return {OO.ui.ActionSet} The widget, for chaining
490  * @fires OO.ui.ActionSet#remove
491  * @fires OO.ui.ActionSet#change
492  */
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 );
501                 }
502         }
503         this.organized = false;
504         this.emit( 'remove', actions );
505         this.changing = false;
506         this.emit( 'change' );
508         return this;
512  * Remove all action widgets from the set.
514  * To remove only specified actions, use the {@link OO.ui.ActionSet#remove remove} method instead.
516  * @chainable
517  * @return {OO.ui.ActionSet} The widget, for chaining
518  * @fires OO.ui.ActionSet#remove
519  * @fires OO.ui.ActionSet#change
520  */
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 );
528         }
530         this.list = [];
532         this.organized = false;
533         this.emit( 'remove', removed );
534         this.changing = false;
535         this.emit( 'change' );
537         return this;
541  * Organize actions.
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.
546  * @private
547  * @chainable
548  * @return {OO.ui.ActionSet} The widget, for chaining
549  */
550 OO.ui.ActionSet.prototype.organize = function () {
551         const specialFlags = this.constructor.static.specialFlags;
553         if ( !this.organized ) {
554                 this.categorized = {};
555                 this.special = {};
556                 this.others = [];
557                 for ( let i = 0, iLen = this.list.length; i < iLen; i++ ) {
558                         const action = this.list[ i ];
559                         let j, jLen;
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 ] = {};
564                                 }
565                                 /**
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}
570                                  */
571                                 let list = action[ this.categories[ category ] ]();
572                                 if ( !Array.isArray( list ) ) {
573                                         list = [ list ];
574                                 }
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 ] = [];
579                                         }
580                                         this.categorized[ category ][ item ].push( action );
581                                 }
582                         }
583                         if ( action.isVisible() ) {
584                                 // Populate special/others
585                                 let special = false;
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;
590                                                 special = true;
591                                                 break;
592                                         }
593                                 }
594                                 if ( !special ) {
595                                         this.others.push( action );
596                                 }
597                         }
598                 }
599                 this.organized = true;
600         }
602         return this;
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
613  * be disabled.
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
622  * @class
624  * @constructor
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.
633  */
634 OO.ui.Error = function OoUiError( message, config ) {
635         // Allow passing positional parameters inside the config object
636         if ( OO.isPlainObject( message ) && config === undefined ) {
637                 config = message;
638                 message = config.message;
639         }
641         // Configuration initialization
642         config = config || {};
644         // Properties
645         this.message = message instanceof $ ? message : String( message );
646         this.recoverable = config.recoverable === undefined || !!config.recoverable;
647         this.warning = !!config.warning;
650 /* Setup */
652 OO.initClass( OO.ui.Error );
654 /* Methods */
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
662  */
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
673  */
674 OO.ui.Error.prototype.isWarning = function () {
675         return this.warning;
679  * Get error message as DOM nodes.
681  * @return {jQuery} Error message in DOM nodes
682  */
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
693  */
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.
713  * @class
715  * @constructor
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.
721  */
722 OO.ui.Process = function ( step, context ) {
723         // Properties
724         this.steps = [];
726         // Initialization
727         if ( step !== undefined ) {
728                 this.next( step, context );
729         }
732 /* Setup */
734 OO.initClass( OO.ui.Process );
736 /* Methods */
739  * Start the 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.
744  */
745 OO.ui.Process.prototype.execute = function () {
746         /**
747          * Continue execution.
748          *
749          * @ignore
750          * @param {Array} step A function and the context it should be called in
751          * @return {Function} Function that continues the process
752          */
753         function proceed( step ) {
754                 return function () {
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();
761                         }
762                         if ( typeof result === 'number' ) {
763                                 if ( result < 0 ) {
764                                         throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
765                                 }
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();
770                         }
771                         if ( result instanceof OO.ui.Error ) {
772                                 // Use rejected promise for error
773                                 return $.Deferred().reject( [ result ] ).promise();
774                         }
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();
778                         }
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();
783                         }
784                         // Use resolved promise for other results
785                         return $.Deferred().resolve().promise();
786                 };
787         }
789         let 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 ] ) );
795                 }
796         } else {
797                 promise = $.Deferred().resolve().promise();
798         }
800         return promise;
804  * Create a process step.
806  * @private
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
820  */
821 OO.ui.Process.prototype.createStep = function ( step, context ) {
822         if ( typeof step === 'number' || typeof step.then === 'function' ) {
823                 return {
824                         callback: function () {
825                                 return step;
826                         },
827                         context: null
828                 };
829         }
830         if ( typeof step === 'function' ) {
831                 return {
832                         callback: step,
833                         context: context
834                 };
835         }
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
844  * @chainable
845  */
846 OO.ui.Process.prototype.first = function ( step, context ) {
847         this.steps.unshift( this.createStep( step, context ) );
848         return this;
852  * Add step to the end of the process.
854  * @inheritdoc #createStep
855  * @return {OO.ui.Process} this
856  * @chainable
857  */
858 OO.ui.Process.prototype.next = function ( step, context ) {
859         this.steps.push( this.createStep( step, context ) );
860         return this;
864  * A window instance represents the life cycle for one single opening of a window
865  * until its closing.
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
874  * @class
876  * @constructor
877  */
878 OO.ui.WindowInstance = function OoUiWindowInstance() {
879         const deferreds = {
880                 opening: $.Deferred(),
881                 opened: $.Deferred(),
882                 closing: $.Deferred(),
883                 closed: $.Deferred()
884         };
886         /**
887          * @private
888          * @property {Object}
889          */
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.
896         /**
897          * @property {jQuery.Promise}
898          */
899         this.opening = deferreds.opening.promise();
900         /**
901          * @property {jQuery.Promise}
902          */
903         this.opened = this.opening.then( () => deferreds.opened );
904         /**
905          * @property {jQuery.Promise}
906          */
907         this.closing = this.opened.then( () => deferreds.closing );
908         /**
909          * @property {jQuery.Promise}
910          */
911         this.closed = this.closing.then( () => deferreds.closed );
914 /* Setup */
916 OO.initClass( OO.ui.WindowInstance );
919  * Check if window is opening.
921  * @return {boolean} Window is opening
922  */
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
931  */
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
941  */
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
951  */
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
1002  * @class
1003  * @extends OO.ui.Element
1004  * @mixes OO.EventEmitter
1006  * @constructor
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.
1014  */
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 );
1025         // Properties
1026         this.factory = config.factory;
1027         this.modal = config.modal === undefined || !!config.modal;
1028         this.windows = {};
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;
1040         this.$inert = 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 );
1046         // Initialization
1047         this.$element
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() ) {
1052                 this.$element
1053                         .attr( 'aria-hidden', 'true' )
1054                         .attr( 'inert', '' );
1055         }
1058 /* Setup */
1060 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
1061 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
1063 /* Events */
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
1073  *  opening data.
1074  * @param {Object} data Window opening data
1075  */
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
1086  */
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
1093  */
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.
1102  * @static
1103  * @property {Object}
1104  */
1105 OO.ui.WindowManager.static.sizes = {
1106         small: {
1107                 width: 300
1108         },
1109         medium: {
1110                 width: 500
1111         },
1112         large: {
1113                 width: 700
1114         },
1115         larger: {
1116                 width: 900
1117         },
1118         full: {
1119                 // These can be non-numeric because they are never used in calculations
1120                 width: '100%',
1121                 height: '100%'
1122         }
1126  * Symbolic name of the default window size.
1128  * The default size is used if the window's requested size is not recognized.
1130  * @static
1131  * @property {string}
1132  */
1133 OO.ui.WindowManager.static.defaultSize = 'medium';
1135 /* Methods */
1138  * Check if the window manager is modal, preventing interaction outside the current window
1140  * @return {boolean} The window manager is modal
1141  */
1142 OO.ui.WindowManager.prototype.isModal = function () {
1143         return this.modal;
1147  * Handle window resize events.
1149  * @private
1150  * @param {jQuery.Event} e Window resize event
1151  */
1152 OO.ui.WindowManager.prototype.onWindowResize = function () {
1153         clearTimeout( this.onWindowResizeTimeout );
1154         this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
1158  * Handle window focus events.
1160  * @private
1161  * @param {jQuery.Event} e Window focus event
1162  */
1163 OO.ui.WindowManager.prototype.onWindowFocus = function () {
1164         const currentWindow = this.getCurrentWindow();
1165         if (
1166                 // This event should only be bound while a window is open
1167                 currentWindow &&
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 )
1172         ) {
1173                 currentWindow.focus();
1174         }
1178  * Handle window resize events.
1180  * @private
1181  * @param {jQuery.Event} e Window resize event
1182  */
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();
1193                 }
1194         }
1198  * Check if window is opening.
1200  * @param {OO.ui.Window} win Window to check
1201  * @return {boolean} Window is opening
1202  */
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
1213  */
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
1224  */
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
1235  */
1236 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
1237         for ( const name in this.windows ) {
1238                 if ( this.windows[ name ] === win ) {
1239                         return true;
1240                 }
1241         }
1243         return false;
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
1252  */
1253 OO.ui.WindowManager.prototype.getSetupDelay = function () {
1254         return 0;
1258  * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’
1259  * process.
1261  * @param {OO.ui.Window} win Window being opened
1262  * @param {Object} [data] Window opening data
1263  * @return {number} Milliseconds to wait
1264  */
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'
1271  * process.
1273  * @param {OO.ui.Window} win Window being closed
1274  * @param {Object} [data] Window closing data
1275  * @return {number} Milliseconds to wait
1276  */
1277 OO.ui.WindowManager.prototype.getHoldDelay = function () {
1278         return 0;
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
1288  */
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.
1305  */
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'
1315                                 ) );
1316                         } else {
1317                                 win = this.factory.create( name );
1318                                 this.addWindows( [ win ] );
1319                                 deferred.resolve( win );
1320                         }
1321                 } else {
1322                         deferred.reject( new OO.ui.Error(
1323                                 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
1324                         ) );
1325                 }
1326         } else {
1327                 deferred.resolve( win );
1328         }
1330         return deferred.promise();
1334  * Get current window.
1336  * @return {OO.ui.Window|null} Currently opening/opened/closing window
1337  */
1338 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
1339         return this.currentWindow;
1343  * Open a window.
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
1356  */
1357 OO.ui.WindowManager.prototype.openWindow = function ( win, data, lifecycle, compatOpening ) {
1358         data = data || {};
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.'
1374                                 );
1375                                 return compatOpening[ method ].apply( this, arguments );
1376                         };
1377                 } );
1379         // Argument handling
1380         if ( typeof win === 'string' ) {
1381                 this.getWindow( win ).then(
1382                         ( w ) => {
1383                                 this.openWindow( w, data, lifecycle, compatOpening );
1384                         },
1385                         ( err ) => {
1386                                 lifecycle.deferreds.opening.reject( err );
1387                         }
1388                 );
1389                 return lifecycle;
1390         }
1392         // Error handling
1393         let error;
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';
1400         }
1402         if ( error ) {
1403                 compatOpening.reject( new OO.ui.Error( error ) );
1404                 lifecycle.deferreds.opening.reject( new OO.ui.Error( error ) );
1405                 return lifecycle;
1406         }
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 );
1415                 }
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 );
1424                 setTimeout( () => {
1425                         this.compatOpened = $.Deferred();
1426                         win.setup( data ).then( () => {
1427                                 compatOpening.notify( { state: 'setup' } );
1428                                 setTimeout( () => {
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 ) {
1439                                                         setTimeout( () => {
1440                                                                 throw dataOrErr;
1441                                                         } );
1442                                                 }
1443                                         } );
1444                                 }, this.getReadyDelay() );
1445                         }, ( dataOrErr ) => {
1446                                 lifecycle.deferreds.opened.reject();
1447                                 compatOpening.reject();
1448                                 this.closeWindow( win );
1449                                 if ( dataOrErr instanceof Error ) {
1450                                         setTimeout( () => {
1451                                                 throw dataOrErr;
1452                                         } );
1453                                 }
1454                         } );
1455                 }, this.getSetupDelay() );
1456         } );
1458         return lifecycle;
1462  * Close a window.
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
1470  */
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 ) ) {
1479                 win = null;
1480         }
1482         // Error handling
1483         let error;
1484         if ( !lifecycle ) {
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';
1492         }
1494         if ( error ) {
1495                 // This function was called for the wrong window and we don't want to mess with the current
1496                 // window's state.
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( {} );
1501         }
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.'
1511                                 );
1512                                 return compatClosing[ method ].apply( this, arguments );
1513                         };
1514                 } );
1516         if ( error ) {
1517                 compatClosing.reject( new OO.ui.Error( error ) );
1518                 lifecycle.deferreds.closing.reject( new OO.ui.Error( error ) );
1519                 return lifecycle;
1520         }
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 );
1533                 setTimeout( () => {
1534                         win.hold( data ).then( () => {
1535                                 compatClosing.notify( { state: 'hold' } );
1536                                 setTimeout( () => {
1537                                         win.teardown( data ).then( () => {
1538                                                 compatClosing.notify( { state: 'teardown' } );
1539                                                 if ( this.isModal() ) {
1540                                                         this.toggleGlobalEvents( false );
1541                                                         this.toggleIsolation( false );
1542                                                 }
1543                                                 if ( this.$returnFocusTo && this.$returnFocusTo.length ) {
1544                                                         this.$returnFocusTo[ 0 ].focus();
1545                                                 }
1546                                                 this.currentWindow = null;
1547                                                 this.lifecycle = null;
1548                                                 lifecycle.deferreds.closed.resolve( data );
1549                                                 compatClosing.resolve( data );
1550                                         } );
1551                                 }, this.getTeardownDelay() );
1552                         } );
1553                 }, this.getHoldDelay() );
1554         } );
1556         return lifecycle;
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.
1585  * Example:
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.
1604  */
1605 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
1606         let list;
1607         if ( Array.isArray( windows ) ) {
1608                 // Convert to map of windows by looking up symbolic names from static configuration
1609                 list = {};
1610                 for ( let i = 0, len = windows.length; i < len; i++ ) {
1611                         const name = windows[ i ].constructor.static.name;
1612                         if ( !name ) {
1613                                 throw new Error( 'Windows must have a `name` static property defined.' );
1614                         }
1615                         list[ name ] = windows[ i ];
1616                 }
1617         } else if ( OO.isPlainObject( windows ) ) {
1618                 list = windows;
1619         }
1621         // Add 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 );
1627         }
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.
1640  */
1641 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
1642         const cleanup = ( name, win ) => {
1643                 delete this.windows[ name ];
1644                 win.$element.detach();
1645         };
1647         const promises = names.map( ( name ) => {
1648                 const win = this.windows[ name ];
1649                 if ( !win ) {
1650                         throw new Error( 'Cannot remove window' );
1651                 }
1652                 const cleanupWindow = cleanup.bind( null, name, win );
1653                 return this.closeWindow( name ).closed.then( cleanupWindow, cleanupWindow );
1654         } );
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
1667  */
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
1678  * @chainable
1679  * @return {OO.ui.WindowManager} The manager, for chaining
1680  */
1681 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
1682         // Bypass for non-current, and thus invisible, windows
1683         if ( win !== this.currentWindow ) {
1684                 return;
1685         }
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
1695         this.$element
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 );
1710         return this;
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.
1719  * @private
1720  * @param {boolean} [on=false]
1721  * @chainable
1722  * @return {OO.ui.WindowManager} The manager, for chaining
1723  */
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 ) {
1731                 return this;
1732         }
1734         const scrollableRoot = OO.ui.Element.static.getRootScrollableElement( $body[ 0 ] );
1736         if ( on ) {
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' );
1743                 }
1744         } else {
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;
1749                 }
1750         }
1751         return this;
1755  * Bind or unbind global events for scrolling/focus.
1757  * @private
1758  * @param {boolean} [on] Bind global events
1759  * @param {OO.ui.Window} [win] The just-opened window (when turning on events)
1760  * @chainable
1761  * @return {OO.ui.WindowManager} The manager, for chaining
1762  */
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() );
1774         if ( on ) {
1775                 if ( !this.globalEvents ) {
1776                         $window.on( {
1777                                 // Start listening for top-level window dimension changes
1778                                 'orientationchange resize': this.onWindowResizeHandler,
1779                                 focus: this.onWindowFocusHandler
1780                         } );
1781                         stack.push( win );
1782                         this.globalEvents = true;
1783                 }
1784         } else if ( this.globalEvents ) {
1785                 $window.off( {
1786                         // Stop listening for top-level window dimension changes
1787                         'orientationchange resize': this.onWindowResizeHandler,
1788                         focus: this.onWindowFocusHandler
1789                 } );
1790                 stack.pop();
1791                 this.globalEvents = false;
1792         }
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' ) );
1797         } else {
1798                 $bodyAndParent.removeClass( 'oo-ui-windowManager-modal-active oo-ui-windowManager-modal-active-fullscreen' );
1799         }
1800         $body.data( 'windowManagerGlobalEvents', stack );
1802         return this;
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).
1811  * @private
1812  * @param {boolean} [isolate] Make only the window manager visible to screen readers
1813  * @chainable
1814  * @return {OO.ui.WindowManager} The manager, for chaining
1815  */
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
1821                 this.$element
1822                         .removeAttr( 'aria-hidden' )
1823                         .removeAttr( 'inert' );
1825                 let $el = this.$element;
1827                 const ariaHidden = [];
1828                 const inert = [];
1830                 // Walk up the tree
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
1835                         // another manager.
1836                         $el
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() );
1846                         $el = $el.parent();
1847                 }
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', '' );
1855         } else {
1856                 // Restore screen reader visibility
1857                 this.$ariaHidden.removeAttr( 'aria-hidden' );
1858                 this.$inert.removeAttr( 'inert' );
1859                 this.$ariaHidden = null;
1860                 this.$inert = null;
1862                 // and hide the window manager
1863                 this.$element
1864                         .attr( 'aria-hidden', 'true' )
1865                         .attr( 'inert', '' );
1866         }
1868         return this;
1872  * Destroy the window manager.
1873  */
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
1901  * window.
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
1910  * asynchronously.
1912  * For more information, please see the [OOUI documentation on MediaWiki][1].
1914  * [1]: https://www.mediawiki.org/wiki/OOUI/Windows
1916  * @abstract
1917  * @class
1918  * @extends OO.ui.Element
1919  * @mixes OO.EventEmitter
1921  * @constructor
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.
1925  */
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 );
1936         // Properties
1937         this.manager = null;
1938         this.size = config.size || this.constructor.static.size;
1939         this.$frame = $( '<div>' );
1940         /**
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>.
1944          *
1945          *     MyDialog.prototype.initialize = function () {
1946          *       ...
1947          *       const popupButton = new OO.ui.PopupButtonWidget( {
1948          *         $overlay: this.$overlay,
1949          *         label: 'Popup button',
1950          *         popup: {
1951          *           $content: $( '<p>Popup content.</p><p>More content.</p><p>Yet more content.</p>' ),
1952          *           padded: true
1953          *         }
1954          *       } );
1955          *       ...
1956          *     };
1957          *
1958          * @property {jQuery}
1959          */
1960         this.$overlay = $( '<div>' );
1961         this.$content = $( '<div>' );
1962         /**
1963          * Set focus traps
1964          *
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.
1967          *
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
1971          */
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 );
1976         // Initialization
1977         this.$overlay.addClass( 'oo-ui-window-overlay' );
1978         this.$content
1979                 .addClass( 'oo-ui-window-content' )
1980                 .attr( 'tabindex', -1 );
1981         this.$frame
1982                 .addClass( 'oo-ui-window-frame' )
1983                 .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
1984         this.$element
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' );
1995 /* Setup */
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.
2007  * @static
2008  * @property {string}
2009  */
2010 OO.ui.Window.static.size = 'medium';
2012 /* Methods */
2015  * Handle mouse down events.
2017  * @private
2018  * @param {jQuery.Event} e Mouse down event
2019  * @return {OO.ui.Window} The window, for chaining
2020  */
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 ] ) {
2024                 return false;
2025         }
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
2034  */
2035 OO.ui.Window.prototype.isInitialized = function () {
2036         return !!this.manager;
2040  * Check if the window is visible.
2042  * @return {boolean} Window is visible
2043  */
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
2055  */
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
2067  */
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
2079  */
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
2091  */
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`
2100  */
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;
2108         }
2109         if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
2110                 size = 'full';
2111         }
2113         return size;
2117  * Get the size properties associated with the current window size
2119  * @return {Object} Size properties
2120  */
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
2127  * back.
2129  * @private
2130  * @param {Function} callback Function to call while transitions are disabled
2131  */
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' );
2143         callback();
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
2162  */
2163 OO.ui.Window.prototype.getContentHeight = function () {
2164         const body = this.$body[ 0 ];
2165         const frame = this.$frame[ 0 ];
2167         let bodyHeight;
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;
2181         } );
2183         return (
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 ) )
2188         );
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
2201  */
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'`
2210  */
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
2221  * values.
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
2225  * of OO.ui.Process.
2227  * To add window content that persists between openings, you may wish to use the #initialize method
2228  * instead.
2230  * @param {Object} [data] Window opening data
2231  * @return {OO.ui.Process} Setup process
2232  */
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
2251  */
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
2266  * of OO.ui.Process.
2268  * @param {Object} [data] Window closing data
2269  * @return {OO.ui.Process} Hold process
2270  */
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
2285  * of OO.ui.Process.
2287  * @param {Object} [data] Window closing data
2288  * @return {OO.ui.Process} Teardown process
2289  */
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
2301  * @chainable
2302  * @return {OO.ui.Window} The window, for chaining
2303  */
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' );
2307         }
2309         this.manager = manager;
2311         this.initialize();
2313         return this;
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
2320  *  `full`
2321  * @chainable
2322  * @return {OO.ui.Window} The window, for chaining
2323  */
2324 OO.ui.Window.prototype.setSize = function ( size ) {
2325         this.size = size;
2326         this.updateSize();
2327         return this;
2331  * Update the window size.
2333  * @throws {Error} An error is thrown if the window is not attached to a window manager
2334  * @chainable
2335  * @return {OO.ui.Window} The window, for chaining
2336  */
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' );
2340         }
2342         this.manager.updateWindowSize( this );
2344         return 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
2360  * @chainable
2361  * @return {OO.ui.Window} The window, for chaining
2362  */
2363 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2364         const styleObj = this.$frame[ 0 ].style;
2366         let height;
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;
2374                 } );
2375         } else {
2376                 height = dim.height;
2377         }
2379         this.$frame.css( {
2380                 width: dim.width || '',
2381                 minWidth: dim.minWidth || '',
2382                 maxWidth: dim.maxWidth || '',
2383                 height: height || '',
2384                 minHeight: dim.minHeight || '',
2385                 maxHeight: dim.maxHeight || ''
2386         } );
2388         return this;
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
2400  * @chainable
2401  * @return {OO.ui.Window} The window, for chaining
2402  */
2403 OO.ui.Window.prototype.initialize = function () {
2404         if ( !this.manager ) {
2405                 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2406         }
2408         // Properties
2409         this.$head = $( '<div>' );
2410         this.$body = $( '<div>' );
2411         this.$foot = $( '<div>' );
2412         this.$document = $( this.getElementDocument() );
2414         // Events
2415         this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2416         this.$focusTraps.on( 'focus', this.onFocusTrapFocused.bind( this ) );
2418         // Initialization
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 );
2424         return this;
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
2432  */
2433 OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
2434         const backwards = this.$focusTrapBefore.is( event.target );
2435         this.focus( backwards );
2439  * Focus the window
2441  * @param {boolean} [focusLast=false] Focus the last focusable element in the window, instead of the first
2442  * @chainable
2443  * @return {OO.ui.Window} The window, for chaining
2444  */
2445 OO.ui.Window.prototype.focus = function ( focusLast ) {
2446         const element = OO.ui.findFocusable( this.$content, !!focusLast );
2447         if ( element ) {
2448                 // There's a focusable element inside the content, at the front or
2449                 // back depending on which focus trap we hit; select it.
2450                 element.focus();
2451         } else {
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' );
2458         }
2459         return this;
2463  * Open the window.
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
2473  */
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' );
2477         }
2479         return this.manager.openWindow( this, data );
2483  * Close the window.
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
2495  */
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' );
2499         }
2501         return this.manager.closeWindow( this, data );
2505  * Setup window.
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
2512  */
2513 OO.ui.Window.prototype.setup = function ( data ) {
2514         this.toggle( true );
2516         return this.getSetupProcess( data ).execute().then( () => {
2517                 this.updateSize();
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();
2521         } );
2525  * Ready window.
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
2532  */
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();
2539         } );
2543  * Hold window.
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
2550  */
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
2556                 );
2558                 // Blur the focused element
2559                 if ( $focus.length ) {
2560                         $focus[ 0 ].blur();
2561                 }
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();
2566         } );
2570  * Teardown window.
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
2577  */
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 );
2584         } );
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.
2594  *     @example
2595  *     // A simple dialog window.
2596  *     function MyDialog( config ) {
2597  *         MyDialog.super.call( this, config );
2598  *     }
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 ' +
2605  *             'close.</p>' );
2606  *         this.$body.append( this.content.$element );
2607  *     };
2608  *     MyDialog.prototype.getBodyHeight = function () {
2609  *         return this.content.$element.outerHeight( true );
2610  *     };
2611  *     const myDialog = new MyDialog( {
2612  *         size: 'medium'
2613  *     } );
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
2623  * @abstract
2624  * @class
2625  * @extends OO.ui.Window
2626  * @mixes OO.ui.mixin.PendingElement
2628  * @constructor
2629  * @param {Object} [config] Configuration options
2630  */
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 );
2638         // Properties
2639         this.actions = new OO.ui.ActionSet();
2640         this.attachedActions = [];
2641         this.currentAction = null;
2642         this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
2644         // Events
2645         this.actions.connect( this, {
2646                 click: 'onActionClick',
2647                 change: 'onActionsChange'
2648         } );
2650         // Initialization
2651         this.$element
2652                 .addClass( 'oo-ui-dialog' )
2653                 .attr( 'role', 'dialog' );
2656 /* Setup */
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
2671  * @abstract
2672  * @static
2673  * @property {string}
2674  */
2675 OO.ui.Dialog.static.name = '';
2678  * The dialog title.
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
2683  * overridden.
2685  * @abstract
2686  * @static
2687  * @property {jQuery|string|Function}
2688  */
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
2699  * @static
2700  * @property {Object[]}
2701  */
2702 OO.ui.Dialog.static.actions = [];
2705  * Close the dialog when the Escape key is pressed.
2707  * @deprecated Have #getEscapeAction return `null` instead
2708  * @static
2709  * @abstract
2710  * @property {boolean}
2711  */
2712 OO.ui.Dialog.static.escapable = true;
2714 /* Methods */
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
2723  */
2724 OO.ui.Dialog.prototype.getEscapeAction = function () {
2725         return '';
2729  * Handle frame document key down events.
2731  * @private
2732  * @param {jQuery.Event} e Key down event
2733  */
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 );
2739                         e.preventDefault();
2740                         e.stopPropagation();
2741                 }
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() );
2746                         e.preventDefault();
2747                         e.stopPropagation();
2748                 }
2749         }
2753  * Handle action click events.
2755  * @private
2756  * @param {OO.ui.ActionWidget} action Action that was clicked
2757  */
2758 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2759         if ( !this.isPending() ) {
2760                 this.executeAction( action.getAction() );
2761         }
2765  * Handle actions change event.
2767  * @private
2768  */
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.
2775                         this.updateSize();
2776                 }
2777         }
2781  * Get the set of actions used by the dialog.
2783  * @return {OO.ui.ActionSet}
2784  */
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
2795  * OO.ui.Process.
2797  * @param {string} [action] Symbolic name of action
2798  * @return {OO.ui.Process} Action process
2799  */
2800 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2801         return new OO.ui.Process()
2802                 .next( () => {
2803                         if ( !action ) {
2804                                 // An empty action always closes the dialog without data, which should always be
2805                                 // safe and make no changes
2806                                 this.close();
2807                         }
2808                 } );
2812  * @inheritdoc
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}.
2819  */
2820 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2821         data = data || {};
2823         // Parent method
2824         return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2825                 .next( () => {
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 );
2834                 } );
2838  * @inheritdoc
2839  */
2840 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2841         // Parent method
2842         return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2843                 .first( () => {
2844                         this.$element.off( 'keydown', this.onDialogKeyDownHandler );
2846                         this.actions.clear();
2847                         this.currentAction = null;
2848                 } );
2852  * @inheritdoc
2853  */
2854 OO.ui.Dialog.prototype.initialize = function () {
2855         // Parent method
2856         OO.ui.Dialog.super.prototype.initialize.call( this );
2858         // Properties
2859         this.title = new OO.ui.LabelWidget();
2861         // Initialization
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
2872  */
2873 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2874         const widgets = [];
2875         for ( let i = 0, len = actions.length; i < len; i++ ) {
2876                 widgets.push( this.getActionWidget( actions[ i ] ) );
2877         }
2878         return widgets;
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
2888  */
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
2900  */
2901 OO.ui.Dialog.prototype.getActionWidgetConfig = function ( config ) {
2902         return config;
2906  * Attach action actions.
2908  * @protected
2909  */
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.
2918  * @protected
2919  * @chainable
2920  * @return {OO.ui.Dialog} The dialog, for chaining
2921  */
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();
2926         }
2927         this.attachedActions = [];
2929         return this;
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
2937  */
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();
2943         }
2944         this.pushPending();
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].
2969  *     @example
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'
2981  *     } );
2983  * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
2985  * @class
2986  * @extends OO.ui.Dialog
2988  * @constructor
2989  * @param {Object} [config] Configuration options
2990  */
2991 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
2992         // Parent constructor
2993         OO.ui.MessageDialog.super.call( this, config );
2995         // Properties
2996         this.verticalActionLayout = null;
2998         // Initialization
2999         this.$element.addClass( 'oo-ui-messageDialog' );
3002 /* Setup */
3004 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
3006 /* Static Properties */
3009  * @static
3010  * @inheritdoc
3011  */
3012 OO.ui.MessageDialog.static.name = 'message';
3015  * @static
3016  * @inheritdoc
3017  */
3018 OO.ui.MessageDialog.static.size = 'small';
3021  * Dialog title.
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.
3026  * @static
3027  * @property {jQuery|string|Function|null}
3028  */
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.
3037  * @static
3038  * @property {jQuery|string|Function|null}
3039  */
3040 OO.ui.MessageDialog.static.message = null;
3043  * @static
3044  * @inheritdoc
3045  */
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' }
3052 /* Methods */
3055  * Toggle action layout between vertical and horizontal.
3057  * @private
3058  * @param {boolean} [value] Layout actions vertically, omit to toggle
3059  * @chainable
3060  * @return {OO.ui.MessageDialog} The dialog, for chaining
3061  */
3062 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
3063         value = value === undefined ? !this.verticalActionLayout : !!value;
3065         if ( value !== this.verticalActionLayout ) {
3066                 this.verticalActionLayout = value;
3067                 this.$actions
3068                         .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
3069                         .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
3070         }
3072         return this;
3076  * @inheritdoc
3077  */
3078 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
3079         if ( action ) {
3080                 return new OO.ui.Process( () => {
3081                         this.close( { action: action } );
3082                 } );
3083         }
3084         return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
3088  * @inheritdoc
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
3095  *  action item
3096  */
3097 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
3098         data = data || {};
3100         // Parent method
3101         return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
3102                 .next( () => {
3103                         this.title.setLabel(
3104                                 data.title !== undefined ? data.title : this.constructor.static.title
3105                         );
3106                         this.message.setLabel(
3107                                 data.message !== undefined ? data.message : this.constructor.static.message
3108                         );
3109                         this.size = data.size !== undefined ? data.size : this.constructor.static.size;
3110                 } );
3114  * @inheritdoc
3115  */
3116 OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
3117         data = data || {};
3119         // Parent method
3120         return OO.ui.MessageDialog.super.prototype.getReadyProcess.call( this, data )
3121                 .next( () => {
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();
3127                         }
3128                 } );
3132  * @inheritdoc
3133  */
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;
3145         return bodyHeight;
3149  * @inheritdoc
3150  */
3151 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
3152         const $scrollable = this.container.$element;
3154         // Parent method
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.
3159         setTimeout( () => {
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();
3171                 }
3173                 $scrollable[ 0 ].style.overflow = oldOverflow;
3174         }, 300 );
3176         this.fitActions();
3177         // Wait for CSS transition to finish and do it again :(
3178         setTimeout( () => {
3179                 this.fitActions();
3180         }, 300 );
3182         return this;
3186  * @inheritdoc
3187  */
3188 OO.ui.MessageDialog.prototype.initialize = function () {
3189         // Parent method
3190         OO.ui.MessageDialog.super.prototype.initialize.call( this );
3192         // Properties
3193         this.$actions = $( '<div>' );
3194         this.container = new OO.ui.PanelLayout( {
3195                 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
3196         } );
3197         this.text = new OO.ui.PanelLayout( {
3198                 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
3199         } );
3200         this.message = new OO.ui.LabelWidget( {
3201                 classes: [ 'oo-ui-messageDialog-message' ]
3202         } );
3204         // Initialization
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 );
3215  * @inheritdoc
3216  */
3217 OO.ui.MessageDialog.prototype.getActionWidgetConfig = function ( config ) {
3218         // Force unframed
3219         return Object.assign( {}, config, { framed: false } );
3223  * @inheritdoc
3224  */
3225 OO.ui.MessageDialog.prototype.attachActions = function () {
3226         // Parent method
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 );
3235         }
3236         for ( let i = 0, len = others.length; i < len; i++ ) {
3237                 this.$actions.append( others[ i ].$element );
3238                 others[ i ].toggleFramed( true );
3239         }
3240         if ( special.primary ) {
3241                 this.$actions.append( special.primary.$element );
3242                 special.primary.toggleFramed( true );
3243         }
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.
3251  * @private
3252  */
3253 OO.ui.MessageDialog.prototype.fitActions = function () {
3254         const previous = this.verticalActionLayout;
3256         // Detect clipping
3257         this.toggleVerticalActionLayout( false );
3258         if ( this.$actions[ 0 ].scrollWidth > this.$actions[ 0 ].clientWidth ) {
3259                 this.toggleVerticalActionLayout( true );
3260         }
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.
3267                 this.updateSize();
3268         }
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.
3287  *     @example
3288  *     // Example: Creating and opening a process dialog window.
3289  *     function MyProcessDialog( config ) {
3290  *         MyProcessDialog.super.call( this, config );
3291  *     }
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' }
3299  *     ];
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 );
3308  *     };
3309  *     MyProcessDialog.prototype.getActionProcess = function ( action ) {
3310  *         if ( action ) {
3311  *             return new OO.ui.Process( () => {
3312  *                 this.close( { action: action } );
3313  *             } );
3314  *         }
3315  *         return MyProcessDialog.super.prototype.getActionProcess.call( this, action );
3316  *     };
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
3327  * @abstract
3328  * @class
3329  * @extends OO.ui.Dialog
3331  * @constructor
3332  * @param {Object} [config] Configuration options
3333  */
3334 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
3335         // Parent constructor
3336         OO.ui.ProcessDialog.super.call( this, config );
3338         // Properties
3339         this.fitOnOpen = false;
3341         // Initialization
3342         this.$element.addClass( 'oo-ui-processDialog' );
3343         if ( OO.ui.isMobile() ) {
3344                 this.$element.addClass( 'oo-ui-isMobile' );
3345         }
3348 /* Setup */
3350 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
3352 /* Methods */
3355  * Handle dismiss button click events.
3357  * Hides errors.
3359  * @private
3360  */
3361 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
3362         this.hideErrors();
3366  * Handle retry button click events.
3368  * Hides errors and then tries again.
3370  * @private
3371  */
3372 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
3373         this.hideErrors();
3374         this.executeAction( this.currentAction );
3378  * @inheritdoc
3379  */
3380 OO.ui.ProcessDialog.prototype.initialize = function () {
3381         // Parent method
3382         OO.ui.ProcessDialog.super.prototype.initialize.call( this );
3384         // Properties
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' )
3392         } );
3393         this.retryButton = new OO.ui.ButtonWidget();
3394         this.$errors = $( '<div>' );
3395         this.$errorsTitle = $( '<div>' );
3397         // Events
3398         this.dismissButton.connect( this, {
3399                 click: 'onDismissErrorButtonClick'
3400         } );
3401         this.retryButton.connect( this, {
3402                 click: 'onRetryButtonClick'
3403         } );
3404         this.title.connect( this, {
3405                 labelChange: 'fitLabel'
3406         } );
3408         // Initialization
3409         this.title.$element.addClass( 'oo-ui-processDialog-title' );
3410         this.$location
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' );
3416         this.$errorsTitle
3417                 .addClass( 'oo-ui-processDialog-errors-title' )
3418                 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
3419         this.$errors
3420                 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
3421                 .append(
3422                         this.$errorsTitle,
3423                         $( '<div>' ).addClass( 'oo-ui-processDialog-errors-actions' ).append(
3424                                 this.dismissButton.$element, this.retryButton.$element
3425                         )
3426                 );
3427         this.$content
3428                 .addClass( 'oo-ui-processDialog-content' )
3429                 .append( this.$errors );
3430         this.$navigation
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"
3435                 // should go first.
3436                 .append( this.$primaryActions, this.$location, this.$safeActions );
3437         this.$head.append( this.$navigation );
3438         this.$foot.append( this.$otherActions );
3442  * @inheritdoc
3443  */
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 );
3448         }
3450         config = Object.assign( { framed: true }, config );
3451         if ( checkFlag( 'close' ) ) {
3452                 // Change close buttons to icon only.
3453                 Object.assign( config, {
3454                         icon: 'close',
3455                         invisibleLabel: true
3456                 } );
3457         } else if ( checkFlag( 'back' ) ) {
3458                 // Change back buttons to icon only.
3459                 Object.assign( config, {
3460                         icon: 'previous',
3461                         invisibleLabel: true
3462                 } );
3463         }
3465         return config;
3469  * @inheritdoc
3470  */
3471 OO.ui.ProcessDialog.prototype.attachActions = function () {
3472         // Parent method
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 );
3479         }
3480         for ( let i = 0, len = others.length; i < len; i++ ) {
3481                 const other = others[ i ];
3482                 this.$otherActions.append( other.$element );
3483         }
3484         if ( special.safe ) {
3485                 this.$safeActions.append( special.safe.$element );
3486         }
3490  * @inheritdoc
3491  */
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 || [] );
3496                 } );
3500  * @inheritdoc
3501  */
3502 OO.ui.ProcessDialog.prototype.setDimensions = function () {
3503         // Parent method
3504         OO.ui.ProcessDialog.super.prototype.setDimensions.apply( this, arguments );
3506         this.fitLabel();
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
3510         // fit them.
3511         this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
3512         // Wait for CSS transition to finish and do it again :(
3513         setTimeout( () => {
3514                 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
3515         }, 300 );
3519  * Fit label between actions.
3521  * @private
3522  * @chainable
3523  * @return {OO.ui.MessageDialog} The dialog, for chaining
3524  */
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;
3538                         }
3539                         return;
3540                 } else {
3541                         return;
3542                 }
3543         } else {
3544                 navigationWidth = size.width - 20;
3545         }
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;
3557         } else {
3558                 // Let's hope we at least have enough space not to overlap, because we can't wrap
3559                 // the label.
3560                 if ( this.getDir() === 'ltr' ) {
3561                         leftWidth = safeWidth;
3562                         rightWidth = primaryWidth;
3563                 } else {
3564                         leftWidth = primaryWidth;
3565                         rightWidth = safeWidth;
3566                 }
3567         }
3569         this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
3571         return this;
3575  * Handle errors that occurred during accept or reject processes.
3577  * @private
3578  * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
3579  */
3580 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
3581         const items = [],
3582                 abilities = {};
3583         let recoverable = true,
3584                 warning = false;
3586         if ( errors instanceof OO.ui.Error ) {
3587                 errors = [ errors ];
3588         }
3590         for ( let i = 0, len = errors.length; i < len; i++ ) {
3591                 if ( !errors[ i ].isRecoverable() ) {
3592                         recoverable = false;
3593                 }
3594                 if ( errors[ i ].isWarning() ) {
3595                         warning = true;
3596                 }
3597                 items.push( new OO.ui.MessageWidget( {
3598                         type: 'error',
3599                         label: errors[ i ].getMessage()
3600                 } ).$element[ 0 ] );
3601         }
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() );
3609                 }
3610         } else {
3611                 abilities[ this.currentAction ] = false;
3612                 this.actions.setAbilities( abilities );
3613         }
3614         if ( warning ) {
3615                 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
3616         } else {
3617                 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
3618         }
3619         this.retryButton.toggle( recoverable );
3620         this.$errorsTitle.after( this.$errorItems );
3621         this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
3625  * Hide errors.
3627  * @private
3628  */
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;
3634         }
3638  * @inheritdoc
3639  */
3640 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
3641         // Parent method
3642         return OO.ui.ProcessDialog.super.prototype.getTeardownProcess.call( this, data )
3643                 .first( () => {
3644                         // Make sure to hide errors.
3645                         this.hideErrors();
3646                         this.fitOnOpen = false;
3647                 } );
3651  * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
3652  * OO.ui.confirm.
3654  * @private
3655  * @return {OO.ui.WindowManager}
3656  */
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() ] );
3662         }
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.
3673  *     @example
3674  *     OO.ui.alert( 'Something happened!' ).done( function () {
3675  *         console.log( 'User closed the dialog.' );
3676  *     } );
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
3683  */
3684 OO.ui.alert = function ( text, options ) {
3685         return OO.ui.getWindowManager().openWindow( 'message', Object.assign( {
3686                 message: text,
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.
3699  *     @example
3700  *     OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
3701  *         if ( confirmed ) {
3702  *             console.log( 'User clicked "OK"!' );
3703  *         } else {
3704  *             console.log( 'User clicked "Cancel" or closed the dialog.' );
3705  *         }
3706  *     } );
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
3712  *  `false`.
3713  */
3714 OO.ui.confirm = function ( text, options ) {
3715         return OO.ui.getWindowManager().openWindow( 'message', Object.assign( {
3716                 message: text
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.
3728  *     @example
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".' );
3734  *         } else {
3735  *             console.log( 'User clicked "Cancel" or closed the dialog.' );
3736  *         }
3737  *     } );
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`.
3746  */
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, {
3751                         align: 'top',
3752                         label: text
3753                 } );
3755         const instance = manager.openWindow( 'message', Object.assign( {
3756                 message: textField.$element
3757         }, options ) );
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' } );
3763                 } );
3764                 textInput.focus();
3765         } );
3767         return instance.closed.then( ( data ) => data && data.action === 'accept' ? textInput.getValue() : null );
3770 }( OO ) );
3772 //# sourceMappingURL=oojs-ui-windows.js.map.json