Merge "Special:Upload should not crash on failing previews"
[mediawiki.git] / resources / lib / oojs-ui / oojs-ui-windows.js
blobab83a55228c64ef6bc030ea4f826b8acb41fcd51
1 /*!
2  * OOjs UI v0.18.4-fix (d4045dee45)
3  * https://www.mediawiki.org/wiki/OOjs_UI
4  *
5  * Copyright 2011–2017 OOjs UI Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2017-01-19T20:22:26Z
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 [OOjs UI documentation on MediaWiki] [1] for more information
22  * and examples.
23  *
24  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
25  *
26  * @class
27  * @extends OO.ui.ButtonWidget
28  * @mixins OO.ui.mixin.PendingElement
29  *
30  * @constructor
31  * @param {Object} [config] Configuration options
32  * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
33  * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
34  *  should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
35  *  for more information about setting modes.
36  * @cfg {boolean} [framed=false] Render the action button with a frame
37  */
38 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
39         // Configuration initialization
40         config = $.extend( { framed: false }, config );
42         // Parent constructor
43         OO.ui.ActionWidget.parent.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 /* Events */
65 /**
66  * A resize event is emitted when the size of the widget changes.
67  *
68  * @event resize
69  */
71 /* Methods */
73 /**
74  * Check if the action is configured to be available in the specified `mode`.
75  *
76  * @param {string} mode Name of mode
77  * @return {boolean} The action is configured with the mode
78  */
79 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
80         return this.modes.indexOf( mode ) !== -1;
83 /**
84  * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
85  *
86  * @return {string}
87  */
88 OO.ui.ActionWidget.prototype.getAction = function () {
89         return this.action;
92 /**
93  * Get the symbolic name of the mode or modes for which the action is configured to be available.
94  *
95  * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
96  * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
97  * are hidden.
98  *
99  * @return {string[]}
100  */
101 OO.ui.ActionWidget.prototype.getModes = function () {
102         return this.modes.slice();
106  * Emit a resize event if the size has changed.
108  * @private
109  * @chainable
110  */
111 OO.ui.ActionWidget.prototype.propagateResize = function () {
112         var width, height;
114         if ( this.isElementAttached() ) {
115                 width = this.$element.width();
116                 height = this.$element.height();
118                 if ( width !== this.width || height !== this.height ) {
119                         this.width = width;
120                         this.height = height;
121                         this.emit( 'resize' );
122                 }
123         }
125         return this;
129  * @inheritdoc
130  */
131 OO.ui.ActionWidget.prototype.setIcon = function () {
132         // Mixin method
133         OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
134         this.propagateResize();
136         return this;
140  * @inheritdoc
141  */
142 OO.ui.ActionWidget.prototype.setLabel = function () {
143         // Mixin method
144         OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
145         this.propagateResize();
147         return this;
151  * @inheritdoc
152  */
153 OO.ui.ActionWidget.prototype.setFlags = function () {
154         // Mixin method
155         OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
156         this.propagateResize();
158         return this;
162  * @inheritdoc
163  */
164 OO.ui.ActionWidget.prototype.clearFlags = function () {
165         // Mixin method
166         OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
167         this.propagateResize();
169         return this;
173  * Toggle the visibility of the action button.
175  * @param {boolean} [show] Show button, omit to toggle visibility
176  * @chainable
177  */
178 OO.ui.ActionWidget.prototype.toggle = function () {
179         // Parent method
180         OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
181         this.propagateResize();
183         return this;
186 /* eslint-disable no-unused-vars */
188  * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
189  * Actions can be made available for specific contexts (modes) and circumstances
190  * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
192  * ActionSets contain two types of actions:
194  * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property.
195  * - Other: Other actions include all non-special visible actions.
197  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
199  *     @example
200  *     // Example: An action set used in a process dialog
201  *     function MyProcessDialog( config ) {
202  *         MyProcessDialog.parent.call( this, config );
203  *     }
204  *     OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
205  *     MyProcessDialog.static.title = 'An action set in a process dialog';
206  *     // An action set that uses modes ('edit' and 'help' mode, in this example).
207  *     MyProcessDialog.static.actions = [
208  *         { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
209  *         { action: 'help', modes: 'edit', label: 'Help' },
210  *         { modes: 'edit', label: 'Cancel', flags: 'safe' },
211  *         { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
212  *     ];
214  *     MyProcessDialog.prototype.initialize = function () {
215  *         MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
216  *         this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
217  *         this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode.</p>' );
218  *         this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
219  *         this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode.</p>' );
220  *         this.stackLayout = new OO.ui.StackLayout( {
221  *             items: [ this.panel1, this.panel2 ]
222  *         } );
223  *         this.$body.append( this.stackLayout.$element );
224  *     };
225  *     MyProcessDialog.prototype.getSetupProcess = function ( data ) {
226  *         return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
227  *             .next( function () {
228  *                 this.actions.setMode( 'edit' );
229  *             }, this );
230  *     };
231  *     MyProcessDialog.prototype.getActionProcess = function ( action ) {
232  *         if ( action === 'help' ) {
233  *             this.actions.setMode( 'help' );
234  *             this.stackLayout.setItem( this.panel2 );
235  *         } else if ( action === 'back' ) {
236  *             this.actions.setMode( 'edit' );
237  *             this.stackLayout.setItem( this.panel1 );
238  *         } else if ( action === 'continue' ) {
239  *             var dialog = this;
240  *             return new OO.ui.Process( function () {
241  *                 dialog.close();
242  *             } );
243  *         }
244  *         return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
245  *     };
246  *     MyProcessDialog.prototype.getBodyHeight = function () {
247  *         return this.panel1.$element.outerHeight( true );
248  *     };
249  *     var windowManager = new OO.ui.WindowManager();
250  *     $( 'body' ).append( windowManager.$element );
251  *     var dialog = new MyProcessDialog( {
252  *         size: 'medium'
253  *     } );
254  *     windowManager.addWindows( [ dialog ] );
255  *     windowManager.openWindow( dialog );
257  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
259  * @abstract
260  * @class
261  * @mixins OO.EventEmitter
263  * @constructor
264  * @param {Object} [config] Configuration options
265  */
266 OO.ui.ActionSet = function OoUiActionSet( config ) {
267         // Configuration initialization
268         config = config || {};
270         // Mixin constructors
271         OO.EventEmitter.call( this );
273         // Properties
274         this.list = [];
275         this.categories = {
276                 actions: 'getAction',
277                 flags: 'getFlags',
278                 modes: 'getModes'
279         };
280         this.categorized = {};
281         this.special = {};
282         this.others = [];
283         this.organized = false;
284         this.changing = false;
285         this.changed = false;
287 /* eslint-enable no-unused-vars */
289 /* Setup */
291 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
293 /* Static Properties */
296  * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
297  *  header of a {@link OO.ui.ProcessDialog process dialog}.
298  *  See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
300  *  [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
302  * @abstract
303  * @static
304  * @inheritable
305  * @property {string}
306  */
307 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
309 /* Events */
312  * @event click
314  * A 'click' event is emitted when an action is clicked.
316  * @param {OO.ui.ActionWidget} action Action that was clicked
317  */
320  * @event resize
322  * A 'resize' event is emitted when an action widget is resized.
324  * @param {OO.ui.ActionWidget} action Action that was resized
325  */
328  * @event add
330  * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
332  * @param {OO.ui.ActionWidget[]} added Actions added
333  */
336  * @event remove
338  * A 'remove' event is emitted when actions are {@link #method-remove removed}
339  *  or {@link #clear cleared}.
341  * @param {OO.ui.ActionWidget[]} added Actions removed
342  */
345  * @event change
347  * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
348  * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
350  */
352 /* Methods */
355  * Handle action change events.
357  * @private
358  * @fires change
359  */
360 OO.ui.ActionSet.prototype.onActionChange = function () {
361         this.organized = false;
362         if ( this.changing ) {
363                 this.changed = true;
364         } else {
365                 this.emit( 'change' );
366         }
370  * Check if an action is one of the special actions.
372  * @param {OO.ui.ActionWidget} action Action to check
373  * @return {boolean} Action is special
374  */
375 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
376         var flag;
378         for ( flag in this.special ) {
379                 if ( action === this.special[ flag ] ) {
380                         return true;
381                 }
382         }
384         return false;
388  * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
389  *  or ‘disabled’.
391  * @param {Object} [filters] Filters to use, omit to get all actions
392  * @param {string|string[]} [filters.actions] Actions that action widgets must have
393  * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
394  * @param {string|string[]} [filters.modes] Modes that action widgets must have
395  * @param {boolean} [filters.visible] Action widgets must be visible
396  * @param {boolean} [filters.disabled] Action widgets must be disabled
397  * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
398  */
399 OO.ui.ActionSet.prototype.get = function ( filters ) {
400         var i, len, list, category, actions, index, match, matches;
402         if ( filters ) {
403                 this.organize();
405                 // Collect category candidates
406                 matches = [];
407                 for ( category in this.categorized ) {
408                         list = filters[ category ];
409                         if ( list ) {
410                                 if ( !Array.isArray( list ) ) {
411                                         list = [ list ];
412                                 }
413                                 for ( i = 0, len = list.length; i < len; i++ ) {
414                                         actions = this.categorized[ category ][ list[ i ] ];
415                                         if ( Array.isArray( actions ) ) {
416                                                 matches.push.apply( matches, actions );
417                                         }
418                                 }
419                         }
420                 }
421                 // Remove by boolean filters
422                 for ( i = 0, len = matches.length; i < len; i++ ) {
423                         match = matches[ i ];
424                         if (
425                                 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
426                                 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
427                         ) {
428                                 matches.splice( i, 1 );
429                                 len--;
430                                 i--;
431                         }
432                 }
433                 // Remove duplicates
434                 for ( i = 0, len = matches.length; i < len; i++ ) {
435                         match = matches[ i ];
436                         index = matches.lastIndexOf( match );
437                         while ( index !== i ) {
438                                 matches.splice( index, 1 );
439                                 len--;
440                                 index = matches.lastIndexOf( match );
441                         }
442                 }
443                 return matches;
444         }
445         return this.list.slice();
449  * Get 'special' actions.
451  * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
452  * Special flags can be configured in subclasses by changing the static #specialFlags property.
454  * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
455  */
456 OO.ui.ActionSet.prototype.getSpecial = function () {
457         this.organize();
458         return $.extend( {}, this.special );
462  * Get 'other' actions.
464  * Other actions include all non-special visible action widgets.
466  * @return {OO.ui.ActionWidget[]} 'Other' action widgets
467  */
468 OO.ui.ActionSet.prototype.getOthers = function () {
469         this.organize();
470         return this.others.slice();
474  * Set the mode  (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
475  * to be available in the specified mode will be made visible. All other actions will be hidden.
477  * @param {string} mode The mode. Only actions configured to be available in the specified
478  *  mode will be made visible.
479  * @chainable
480  * @fires toggle
481  * @fires change
482  */
483 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
484         var i, len, action;
486         this.changing = true;
487         for ( i = 0, len = this.list.length; i < len; i++ ) {
488                 action = this.list[ i ];
489                 action.toggle( action.hasMode( mode ) );
490         }
492         this.organized = false;
493         this.changing = false;
494         this.emit( 'change' );
496         return this;
500  * Set the abilities of the specified actions.
502  * Action widgets that are configured with the specified actions will be enabled
503  * or disabled based on the boolean values specified in the `actions`
504  * parameter.
506  * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
507  *  values that indicate whether or not the action should be enabled.
508  * @chainable
509  */
510 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
511         var i, len, action, item;
513         for ( i = 0, len = this.list.length; i < len; i++ ) {
514                 item = this.list[ i ];
515                 action = item.getAction();
516                 if ( actions[ action ] !== undefined ) {
517                         item.setDisabled( !actions[ action ] );
518                 }
519         }
521         return this;
525  * Executes a function once per action.
527  * When making changes to multiple actions, use this method instead of iterating over the actions
528  * manually to defer emitting a #change event until after all actions have been changed.
530  * @param {Object|null} filter Filters to use to determine which actions to iterate over; see #get
531  * @param {Function} callback Callback to run for each action; callback is invoked with three
532  *   arguments: the action, the action's index, the list of actions being iterated over
533  * @chainable
534  */
535 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
536         this.changed = false;
537         this.changing = true;
538         this.get( filter ).forEach( callback );
539         this.changing = false;
540         if ( this.changed ) {
541                 this.emit( 'change' );
542         }
544         return this;
548  * Add action widgets to the action set.
550  * @param {OO.ui.ActionWidget[]} actions Action widgets to add
551  * @chainable
552  * @fires add
553  * @fires change
554  */
555 OO.ui.ActionSet.prototype.add = function ( actions ) {
556         var i, len, action;
558         this.changing = true;
559         for ( i = 0, len = actions.length; i < len; i++ ) {
560                 action = actions[ i ];
561                 action.connect( this, {
562                         click: [ 'emit', 'click', action ],
563                         resize: [ 'emit', 'resize', action ],
564                         toggle: [ 'onActionChange' ]
565                 } );
566                 this.list.push( action );
567         }
568         this.organized = false;
569         this.emit( 'add', actions );
570         this.changing = false;
571         this.emit( 'change' );
573         return this;
577  * Remove action widgets from the set.
579  * To remove all actions, you may wish to use the #clear method instead.
581  * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
582  * @chainable
583  * @fires remove
584  * @fires change
585  */
586 OO.ui.ActionSet.prototype.remove = function ( actions ) {
587         var i, len, index, action;
589         this.changing = true;
590         for ( i = 0, len = actions.length; i < len; i++ ) {
591                 action = actions[ i ];
592                 index = this.list.indexOf( action );
593                 if ( index !== -1 ) {
594                         action.disconnect( this );
595                         this.list.splice( index, 1 );
596                 }
597         }
598         this.organized = false;
599         this.emit( 'remove', actions );
600         this.changing = false;
601         this.emit( 'change' );
603         return this;
607  * Remove all action widets from the set.
609  * To remove only specified actions, use the {@link #method-remove remove} method instead.
611  * @chainable
612  * @fires remove
613  * @fires change
614  */
615 OO.ui.ActionSet.prototype.clear = function () {
616         var i, len, action,
617                 removed = this.list.slice();
619         this.changing = true;
620         for ( i = 0, len = this.list.length; i < len; i++ ) {
621                 action = this.list[ i ];
622                 action.disconnect( this );
623         }
625         this.list = [];
627         this.organized = false;
628         this.emit( 'remove', removed );
629         this.changing = false;
630         this.emit( 'change' );
632         return this;
636  * Organize actions.
638  * This is called whenever organized information is requested. It will only reorganize the actions
639  * if something has changed since the last time it ran.
641  * @private
642  * @chainable
643  */
644 OO.ui.ActionSet.prototype.organize = function () {
645         var i, iLen, j, jLen, flag, action, category, list, item, special,
646                 specialFlags = this.constructor.static.specialFlags;
648         if ( !this.organized ) {
649                 this.categorized = {};
650                 this.special = {};
651                 this.others = [];
652                 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
653                         action = this.list[ i ];
654                         if ( action.isVisible() ) {
655                                 // Populate categories
656                                 for ( category in this.categories ) {
657                                         if ( !this.categorized[ category ] ) {
658                                                 this.categorized[ category ] = {};
659                                         }
660                                         list = action[ this.categories[ category ] ]();
661                                         if ( !Array.isArray( list ) ) {
662                                                 list = [ list ];
663                                         }
664                                         for ( j = 0, jLen = list.length; j < jLen; j++ ) {
665                                                 item = list[ j ];
666                                                 if ( !this.categorized[ category ][ item ] ) {
667                                                         this.categorized[ category ][ item ] = [];
668                                                 }
669                                                 this.categorized[ category ][ item ].push( action );
670                                         }
671                                 }
672                                 // Populate special/others
673                                 special = false;
674                                 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
675                                         flag = specialFlags[ j ];
676                                         if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
677                                                 this.special[ flag ] = action;
678                                                 special = true;
679                                                 break;
680                                         }
681                                 }
682                                 if ( !special ) {
683                                         this.others.push( action );
684                                 }
685                         }
686                 }
687                 this.organized = true;
688         }
690         return this;
694  * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
695  * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
696  * appearance and functionality of the error interface.
698  * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
699  * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
700  * that initiated the failed process will be disabled.
702  * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
703  * process again.
705  * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
707  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
709  * @class
711  * @constructor
712  * @param {string|jQuery} message Description of error
713  * @param {Object} [config] Configuration options
714  * @cfg {boolean} [recoverable=true] Error is recoverable.
715  *  By default, errors are recoverable, and users can try the process again.
716  * @cfg {boolean} [warning=false] Error is a warning.
717  *  If the error is a warning, the error interface will include a
718  *  'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
719  *  is not triggered a second time if the user chooses to continue.
720  */
721 OO.ui.Error = function OoUiError( message, config ) {
722         // Allow passing positional parameters inside the config object
723         if ( OO.isPlainObject( message ) && config === undefined ) {
724                 config = message;
725                 message = config.message;
726         }
728         // Configuration initialization
729         config = config || {};
731         // Properties
732         this.message = message instanceof jQuery ? message : String( message );
733         this.recoverable = config.recoverable === undefined || !!config.recoverable;
734         this.warning = !!config.warning;
737 /* Setup */
739 OO.initClass( OO.ui.Error );
741 /* Methods */
744  * Check if the error is recoverable.
746  * If the error is recoverable, users are able to try the process again.
748  * @return {boolean} Error is recoverable
749  */
750 OO.ui.Error.prototype.isRecoverable = function () {
751         return this.recoverable;
755  * Check if the error is a warning.
757  * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
759  * @return {boolean} Error is warning
760  */
761 OO.ui.Error.prototype.isWarning = function () {
762         return this.warning;
766  * Get error message as DOM nodes.
768  * @return {jQuery} Error message in DOM nodes
769  */
770 OO.ui.Error.prototype.getMessage = function () {
771         return this.message instanceof jQuery ?
772                 this.message.clone() :
773                 $( '<div>' ).text( this.message ).contents();
777  * Get the error message text.
779  * @return {string} Error message
780  */
781 OO.ui.Error.prototype.getMessageText = function () {
782         return this.message instanceof jQuery ? this.message.text() : this.message;
786  * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
787  * or a function:
789  * - **number**: the process will wait for the specified number of milliseconds before proceeding.
790  * - **promise**: the process will continue to the next step when the promise is successfully resolved
791  *  or stop if the promise is rejected.
792  * - **function**: the process will execute the function. The process will stop if the function returns
793  *  either a boolean `false` or a promise that is rejected; if the function returns a number, the process
794  *  will wait for that number of milliseconds before proceeding.
796  * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
797  * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
798  * its remaining steps will not be performed.
800  * @class
802  * @constructor
803  * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
804  *  that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
805  * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
806  *  a number or promise.
807  */
808 OO.ui.Process = function ( step, context ) {
809         // Properties
810         this.steps = [];
812         // Initialization
813         if ( step !== undefined ) {
814                 this.next( step, context );
815         }
818 /* Setup */
820 OO.initClass( OO.ui.Process );
822 /* Methods */
825  * Start the process.
827  * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
828  *  If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
829  *  and any remaining steps are not performed.
830  */
831 OO.ui.Process.prototype.execute = function () {
832         var i, len, promise;
834         /**
835          * Continue execution.
836          *
837          * @ignore
838          * @param {Array} step A function and the context it should be called in
839          * @return {Function} Function that continues the process
840          */
841         function proceed( step ) {
842                 return function () {
843                         // Execute step in the correct context
844                         var deferred,
845                                 result = step.callback.call( step.context );
847                         if ( result === false ) {
848                                 // Use rejected promise for boolean false results
849                                 return $.Deferred().reject( [] ).promise();
850                         }
851                         if ( typeof result === 'number' ) {
852                                 if ( result < 0 ) {
853                                         throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
854                                 }
855                                 // Use a delayed promise for numbers, expecting them to be in milliseconds
856                                 deferred = $.Deferred();
857                                 setTimeout( deferred.resolve, result );
858                                 return deferred.promise();
859                         }
860                         if ( result instanceof OO.ui.Error ) {
861                                 // Use rejected promise for error
862                                 return $.Deferred().reject( [ result ] ).promise();
863                         }
864                         if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
865                                 // Use rejected promise for list of errors
866                                 return $.Deferred().reject( result ).promise();
867                         }
868                         // Duck-type the object to see if it can produce a promise
869                         if ( result && $.isFunction( result.promise ) ) {
870                                 // Use a promise generated from the result
871                                 return result.promise();
872                         }
873                         // Use resolved promise for other results
874                         return $.Deferred().resolve().promise();
875                 };
876         }
878         if ( this.steps.length ) {
879                 // Generate a chain reaction of promises
880                 promise = proceed( this.steps[ 0 ] )();
881                 for ( i = 1, len = this.steps.length; i < len; i++ ) {
882                         promise = promise.then( proceed( this.steps[ i ] ) );
883                 }
884         } else {
885                 promise = $.Deferred().resolve().promise();
886         }
888         return promise;
892  * Create a process step.
894  * @private
895  * @param {number|jQuery.Promise|Function} step
897  * - Number of milliseconds to wait before proceeding
898  * - Promise that must be resolved before proceeding
899  * - Function to execute
900  *   - If the function returns a boolean false the process will stop
901  *   - If the function returns a promise, the process will continue to the next
902  *     step when the promise is resolved or stop if the promise is rejected
903  *   - If the function returns a number, the process will wait for that number of
904  *     milliseconds before proceeding
905  * @param {Object} [context=null] Execution context of the function. The context is
906  *  ignored if the step is a number or promise.
907  * @return {Object} Step object, with `callback` and `context` properties
908  */
909 OO.ui.Process.prototype.createStep = function ( step, context ) {
910         if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
911                 return {
912                         callback: function () {
913                                 return step;
914                         },
915                         context: null
916                 };
917         }
918         if ( $.isFunction( step ) ) {
919                 return {
920                         callback: step,
921                         context: context
922                 };
923         }
924         throw new Error( 'Cannot create process step: number, promise or function expected' );
928  * Add step to the beginning of the process.
930  * @inheritdoc #createStep
931  * @return {OO.ui.Process} this
932  * @chainable
933  */
934 OO.ui.Process.prototype.first = function ( step, context ) {
935         this.steps.unshift( this.createStep( step, context ) );
936         return this;
940  * Add step to the end of the process.
942  * @inheritdoc #createStep
943  * @return {OO.ui.Process} this
944  * @chainable
945  */
946 OO.ui.Process.prototype.next = function ( step, context ) {
947         this.steps.push( this.createStep( step, context ) );
948         return this;
952  * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
953  * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
954  * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
955  * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
956  * pertinent data and reused.
958  * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
959  * `opened`, and `closing`, which represent the primary stages of the cycle:
961  * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
962  * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
964  * - an `opening` event is emitted with an `opening` promise
965  * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
966  *   the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
967  *   window and its result executed
968  * - a `setup` progress notification is emitted from the `opening` promise
969  * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
970  *   the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
971  *   window and its result executed
972  * - a `ready` progress notification is emitted from the `opening` promise
973  * - the `opening` promise is resolved with an `opened` promise
975  * **Opened**: the window is now open.
977  * **Closing**: the closing stage begins when the window manager's #closeWindow or the
978  * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
979  * to close the window.
981  * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
982  * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
983  *   the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
984  *   window and its result executed
985  * - a `hold` progress notification is emitted from the `closing` promise
986  * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
987  *   the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
988  *   window and its result executed
989  * - a `teardown` progress notification is emitted from the `closing` promise
990  * - the `closing` promise is resolved. The window is now closed
992  * See the [OOjs UI documentation on MediaWiki][1] for more information.
994  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
996  * @class
997  * @extends OO.ui.Element
998  * @mixins OO.EventEmitter
1000  * @constructor
1001  * @param {Object} [config] Configuration options
1002  * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
1003  *  Note that window classes that are instantiated with a factory must have
1004  *  a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
1005  * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
1006  */
1007 OO.ui.WindowManager = function OoUiWindowManager( config ) {
1008         // Configuration initialization
1009         config = config || {};
1011         // Parent constructor
1012         OO.ui.WindowManager.parent.call( this, config );
1014         // Mixin constructors
1015         OO.EventEmitter.call( this );
1017         // Properties
1018         this.factory = config.factory;
1019         this.modal = config.modal === undefined || !!config.modal;
1020         this.windows = {};
1021         this.opening = null;
1022         this.opened = null;
1023         this.closing = null;
1024         this.preparingToOpen = null;
1025         this.preparingToClose = null;
1026         this.currentWindow = null;
1027         this.globalEvents = false;
1028         this.$returnFocusTo = null;
1029         this.$ariaHidden = null;
1030         this.onWindowResizeTimeout = null;
1031         this.onWindowResizeHandler = this.onWindowResize.bind( this );
1032         this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
1034         // Initialization
1035         this.$element
1036                 .addClass( 'oo-ui-windowManager' )
1037                 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
1040 /* Setup */
1042 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
1043 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
1045 /* Events */
1048  * An 'opening' event is emitted when the window begins to be opened.
1050  * @event opening
1051  * @param {OO.ui.Window} win Window that's being opened
1052  * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
1053  *  When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
1054  *  is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
1055  * @param {Object} data Window opening data
1056  */
1059  * A 'closing' event is emitted when the window begins to be closed.
1061  * @event closing
1062  * @param {OO.ui.Window} win Window that's being closed
1063  * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
1064  *  is closed successfully. The promise emits `hold` and `teardown` notifications when those
1065  *  processes are complete. When the `closing` promise is resolved, the first argument of its value
1066  *  is the closing data.
1067  * @param {Object} data Window closing data
1068  */
1071  * A 'resize' event is emitted when a window is resized.
1073  * @event resize
1074  * @param {OO.ui.Window} win Window that was resized
1075  */
1077 /* Static Properties */
1080  * Map of the symbolic name of each window size and its CSS properties.
1082  * @static
1083  * @inheritable
1084  * @property {Object}
1085  */
1086 OO.ui.WindowManager.static.sizes = {
1087         small: {
1088                 width: 300
1089         },
1090         medium: {
1091                 width: 500
1092         },
1093         large: {
1094                 width: 700
1095         },
1096         larger: {
1097                 width: 900
1098         },
1099         full: {
1100                 // These can be non-numeric because they are never used in calculations
1101                 width: '100%',
1102                 height: '100%'
1103         }
1107  * Symbolic name of the default window size.
1109  * The default size is used if the window's requested size is not recognized.
1111  * @static
1112  * @inheritable
1113  * @property {string}
1114  */
1115 OO.ui.WindowManager.static.defaultSize = 'medium';
1117 /* Methods */
1120  * Handle window resize events.
1122  * @private
1123  * @param {jQuery.Event} e Window resize event
1124  */
1125 OO.ui.WindowManager.prototype.onWindowResize = function () {
1126         clearTimeout( this.onWindowResizeTimeout );
1127         this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
1131  * Handle window resize events.
1133  * @private
1134  * @param {jQuery.Event} e Window resize event
1135  */
1136 OO.ui.WindowManager.prototype.afterWindowResize = function () {
1137         if ( this.currentWindow ) {
1138                 this.updateWindowSize( this.currentWindow );
1139         }
1143  * Check if window is opening.
1145  * @param {OO.ui.Window} win Window to check
1146  * @return {boolean} Window is opening
1147  */
1148 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
1149         return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
1153  * Check if window is closing.
1155  * @param {OO.ui.Window} win Window to check
1156  * @return {boolean} Window is closing
1157  */
1158 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
1159         return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
1163  * Check if window is opened.
1165  * @param {OO.ui.Window} win Window to check
1166  * @return {boolean} Window is opened
1167  */
1168 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
1169         return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
1173  * Check if a window is being managed.
1175  * @param {OO.ui.Window} win Window to check
1176  * @return {boolean} Window is being managed
1177  */
1178 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
1179         var name;
1181         for ( name in this.windows ) {
1182                 if ( this.windows[ name ] === win ) {
1183                         return true;
1184                 }
1185         }
1187         return false;
1191  * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
1193  * @param {OO.ui.Window} win Window being opened
1194  * @param {Object} [data] Window opening data
1195  * @return {number} Milliseconds to wait
1196  */
1197 OO.ui.WindowManager.prototype.getSetupDelay = function () {
1198         return 0;
1202  * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
1204  * @param {OO.ui.Window} win Window being opened
1205  * @param {Object} [data] Window opening data
1206  * @return {number} Milliseconds to wait
1207  */
1208 OO.ui.WindowManager.prototype.getReadyDelay = function () {
1209         return 0;
1213  * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
1215  * @param {OO.ui.Window} win Window being closed
1216  * @param {Object} [data] Window closing data
1217  * @return {number} Milliseconds to wait
1218  */
1219 OO.ui.WindowManager.prototype.getHoldDelay = function () {
1220         return 0;
1224  * Get the number of milliseconds to wait after the ‘hold’ process has finished before
1225  * executing the ‘teardown’ process.
1227  * @param {OO.ui.Window} win Window being closed
1228  * @param {Object} [data] Window closing data
1229  * @return {number} Milliseconds to wait
1230  */
1231 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
1232         return this.modal ? 250 : 0;
1236  * Get a window by its symbolic name.
1238  * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
1239  * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
1240  * for more information about using factories.
1241  * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
1243  * @param {string} name Symbolic name of the window
1244  * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
1245  * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
1246  * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
1247  */
1248 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
1249         var deferred = $.Deferred(),
1250                 win = this.windows[ name ];
1252         if ( !( win instanceof OO.ui.Window ) ) {
1253                 if ( this.factory ) {
1254                         if ( !this.factory.lookup( name ) ) {
1255                                 deferred.reject( new OO.ui.Error(
1256                                         'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
1257                                 ) );
1258                         } else {
1259                                 win = this.factory.create( name );
1260                                 this.addWindows( [ win ] );
1261                                 deferred.resolve( win );
1262                         }
1263                 } else {
1264                         deferred.reject( new OO.ui.Error(
1265                                 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
1266                         ) );
1267                 }
1268         } else {
1269                 deferred.resolve( win );
1270         }
1272         return deferred.promise();
1276  * Get current window.
1278  * @return {OO.ui.Window|null} Currently opening/opened/closing window
1279  */
1280 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
1281         return this.currentWindow;
1285  * Open a window.
1287  * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
1288  * @param {Object} [data] Window opening data
1289  * @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when closed.
1290  *  Defaults the current activeElement. If set to null, focus isn't changed on close.
1291  * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
1292  *  See {@link #event-opening 'opening' event}  for more information about `opening` promises.
1293  * @fires opening
1294  */
1295 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
1296         var manager = this,
1297                 opening = $.Deferred();
1298         data = data || {};
1300         // Argument handling
1301         if ( typeof win === 'string' ) {
1302                 return this.getWindow( win ).then( function ( win ) {
1303                         return manager.openWindow( win, data );
1304                 } );
1305         }
1307         // Error handling
1308         if ( !this.hasWindow( win ) ) {
1309                 opening.reject( new OO.ui.Error(
1310                         'Cannot open window: window is not attached to manager'
1311                 ) );
1312         } else if ( this.preparingToOpen || this.opening || this.opened ) {
1313                 opening.reject( new OO.ui.Error(
1314                         'Cannot open window: another window is opening or open'
1315                 ) );
1316         }
1318         // Window opening
1319         if ( opening.state() !== 'rejected' ) {
1320                 // If a window is currently closing, wait for it to complete
1321                 this.preparingToOpen = $.when( this.closing );
1322                 // Ensure handlers get called after preparingToOpen is set
1323                 this.preparingToOpen.done( function () {
1324                         if ( manager.modal ) {
1325                                 manager.toggleGlobalEvents( true );
1326                                 manager.toggleAriaIsolation( true );
1327                         }
1328                         manager.$returnFocusTo = data.$returnFocusTo || $( document.activeElement );
1329                         manager.currentWindow = win;
1330                         manager.opening = opening;
1331                         manager.preparingToOpen = null;
1332                         manager.emit( 'opening', win, opening, data );
1333                         setTimeout( function () {
1334                                 win.setup( data ).then( function () {
1335                                         manager.updateWindowSize( win );
1336                                         manager.opening.notify( { state: 'setup' } );
1337                                         setTimeout( function () {
1338                                                 win.ready( data ).then( function () {
1339                                                         manager.opening.notify( { state: 'ready' } );
1340                                                         manager.opening = null;
1341                                                         manager.opened = $.Deferred();
1342                                                         opening.resolve( manager.opened.promise(), data );
1343                                                 }, function () {
1344                                                         manager.opening = null;
1345                                                         manager.opened = $.Deferred();
1346                                                         opening.reject();
1347                                                         manager.closeWindow( win );
1348                                                 } );
1349                                         }, manager.getReadyDelay() );
1350                                 }, function () {
1351                                         manager.opening = null;
1352                                         manager.opened = $.Deferred();
1353                                         opening.reject();
1354                                         manager.closeWindow( win );
1355                                 } );
1356                         }, manager.getSetupDelay() );
1357                 } );
1358         }
1360         return opening.promise();
1364  * Close a window.
1366  * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
1367  * @param {Object} [data] Window closing data
1368  * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
1369  *  See {@link #event-closing 'closing' event} for more information about closing promises.
1370  * @throws {Error} An error is thrown if the window is not managed by the window manager.
1371  * @fires closing
1372  */
1373 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
1374         var manager = this,
1375                 closing = $.Deferred(),
1376                 opened;
1378         // Argument handling
1379         if ( typeof win === 'string' ) {
1380                 win = this.windows[ win ];
1381         } else if ( !this.hasWindow( win ) ) {
1382                 win = null;
1383         }
1385         // Error handling
1386         if ( !win ) {
1387                 closing.reject( new OO.ui.Error(
1388                         'Cannot close window: window is not attached to manager'
1389                 ) );
1390         } else if ( win !== this.currentWindow ) {
1391                 closing.reject( new OO.ui.Error(
1392                         'Cannot close window: window already closed with different data'
1393                 ) );
1394         } else if ( this.preparingToClose || this.closing ) {
1395                 closing.reject( new OO.ui.Error(
1396                         'Cannot close window: window already closing with different data'
1397                 ) );
1398         }
1400         // Window closing
1401         if ( closing.state() !== 'rejected' ) {
1402                 // If the window is currently opening, close it when it's done
1403                 this.preparingToClose = $.when( this.opening );
1404                 // Ensure handlers get called after preparingToClose is set
1405                 this.preparingToClose.always( function () {
1406                         manager.closing = closing;
1407                         manager.preparingToClose = null;
1408                         manager.emit( 'closing', win, closing, data );
1409                         opened = manager.opened;
1410                         manager.opened = null;
1411                         opened.resolve( closing.promise(), data );
1412                         setTimeout( function () {
1413                                 win.hold( data ).then( function () {
1414                                         closing.notify( { state: 'hold' } );
1415                                         setTimeout( function () {
1416                                                 win.teardown( data ).then( function () {
1417                                                         closing.notify( { state: 'teardown' } );
1418                                                         if ( manager.modal ) {
1419                                                                 manager.toggleGlobalEvents( false );
1420                                                                 manager.toggleAriaIsolation( false );
1421                                                         }
1422                                                         if ( manager.$returnFocusTo && manager.$returnFocusTo.length ) {
1423                                                                 manager.$returnFocusTo[ 0 ].focus();
1424                                                         }
1425                                                         manager.closing = null;
1426                                                         manager.currentWindow = null;
1427                                                         closing.resolve( data );
1428                                                 } );
1429                                         }, manager.getTeardownDelay() );
1430                                 } );
1431                         }, manager.getHoldDelay() );
1432                 } );
1433         }
1435         return closing.promise();
1439  * Add windows to the window manager.
1441  * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
1442  * See the [OOjs ui documentation on MediaWiki] [2] for examples.
1443  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
1445  * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
1446  *  by reference, symbolic name, or explicitly defined symbolic names.
1447  * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
1448  *  explicit nor a statically configured symbolic name.
1449  */
1450 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
1451         var i, len, win, name, list;
1453         if ( Array.isArray( windows ) ) {
1454                 // Convert to map of windows by looking up symbolic names from static configuration
1455                 list = {};
1456                 for ( i = 0, len = windows.length; i < len; i++ ) {
1457                         name = windows[ i ].constructor.static.name;
1458                         if ( typeof name !== 'string' ) {
1459                                 throw new Error( 'Cannot add window' );
1460                         }
1461                         if ( !name ) {
1462                                 OO.ui.warnDeprecation( 'OO.ui.WindowManager#addWindows: Windows must have a `name` static property defined.' );
1463                         }
1464                         list[ name ] = windows[ i ];
1465                 }
1466         } else if ( OO.isPlainObject( windows ) ) {
1467                 list = windows;
1468         }
1470         // Add windows
1471         for ( name in list ) {
1472                 win = list[ name ];
1473                 this.windows[ name ] = win.toggle( false );
1474                 this.$element.append( win.$element );
1475                 win.setManager( this );
1476         }
1480  * Remove the specified windows from the windows manager.
1482  * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
1483  * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
1484  * longer listens to events, use the #destroy method.
1486  * @param {string[]} names Symbolic names of windows to remove
1487  * @return {jQuery.Promise} Promise resolved when window is closed and removed
1488  * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
1489  */
1490 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
1491         var i, len, win, name, cleanupWindow,
1492                 manager = this,
1493                 promises = [],
1494                 cleanup = function ( name, win ) {
1495                         delete manager.windows[ name ];
1496                         win.$element.detach();
1497                 };
1499         for ( i = 0, len = names.length; i < len; i++ ) {
1500                 name = names[ i ];
1501                 win = this.windows[ name ];
1502                 if ( !win ) {
1503                         throw new Error( 'Cannot remove window' );
1504                 }
1505                 cleanupWindow = cleanup.bind( null, name, win );
1506                 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
1507         }
1509         return $.when.apply( $, promises );
1513  * Remove all windows from the window manager.
1515  * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
1516  * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
1517  * To remove just a subset of windows, use the #removeWindows method.
1519  * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
1520  */
1521 OO.ui.WindowManager.prototype.clearWindows = function () {
1522         return this.removeWindows( Object.keys( this.windows ) );
1526  * Set dialog size. In general, this method should not be called directly.
1528  * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
1530  * @param {OO.ui.Window} win Window to update, should be the current window
1531  * @chainable
1532  */
1533 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
1534         var isFullscreen;
1536         // Bypass for non-current, and thus invisible, windows
1537         if ( win !== this.currentWindow ) {
1538                 return;
1539         }
1541         isFullscreen = win.getSize() === 'full';
1543         this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
1544         this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
1545         win.setDimensions( win.getSizeProperties() );
1547         this.emit( 'resize', win );
1549         return this;
1553  * Bind or unbind global events for scrolling.
1555  * @private
1556  * @param {boolean} [on] Bind global events
1557  * @chainable
1558  */
1559 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
1560         var scrollWidth, bodyMargin,
1561                 $body = $( this.getElementDocument().body ),
1562                 // We could have multiple window managers open so only modify
1563                 // the body css at the bottom of the stack
1564                 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0;
1566         on = on === undefined ? !!this.globalEvents : !!on;
1568         if ( on ) {
1569                 if ( !this.globalEvents ) {
1570                         $( this.getElementWindow() ).on( {
1571                                 // Start listening for top-level window dimension changes
1572                                 'orientationchange resize': this.onWindowResizeHandler
1573                         } );
1574                         if ( stackDepth === 0 ) {
1575                                 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
1576                                 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
1577                                 $body.css( {
1578                                         overflow: 'hidden',
1579                                         'margin-right': bodyMargin + scrollWidth
1580                                 } );
1581                         }
1582                         stackDepth++;
1583                         this.globalEvents = true;
1584                 }
1585         } else if ( this.globalEvents ) {
1586                 $( this.getElementWindow() ).off( {
1587                         // Stop listening for top-level window dimension changes
1588                         'orientationchange resize': this.onWindowResizeHandler
1589                 } );
1590                 stackDepth--;
1591                 if ( stackDepth === 0 ) {
1592                         $body.css( {
1593                                 overflow: '',
1594                                 'margin-right': ''
1595                         } );
1596                 }
1597                 this.globalEvents = false;
1598         }
1599         $body.data( 'windowManagerGlobalEvents', stackDepth );
1601         return this;
1605  * Toggle screen reader visibility of content other than the window manager.
1607  * @private
1608  * @param {boolean} [isolate] Make only the window manager visible to screen readers
1609  * @chainable
1610  */
1611 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
1612         isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
1614         if ( isolate ) {
1615                 if ( !this.$ariaHidden ) {
1616                         // Hide everything other than the window manager from screen readers
1617                         this.$ariaHidden = $( 'body' )
1618                                 .children()
1619                                 .not( this.$element.parentsUntil( 'body' ).last() )
1620                                 .attr( 'aria-hidden', '' );
1621                 }
1622         } else if ( this.$ariaHidden ) {
1623                 // Restore screen reader visibility
1624                 this.$ariaHidden.removeAttr( 'aria-hidden' );
1625                 this.$ariaHidden = null;
1626         }
1628         return this;
1632  * Destroy the window manager.
1634  * Destroying the window manager ensures that it will no longer listen to events. If you would like to
1635  * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
1636  * instead.
1637  */
1638 OO.ui.WindowManager.prototype.destroy = function () {
1639         this.toggleGlobalEvents( false );
1640         this.toggleAriaIsolation( false );
1641         this.clearWindows();
1642         this.$element.remove();
1646  * A window is a container for elements that are in a child frame. They are used with
1647  * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1648  * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1649  * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1650  * the window manager will choose a sensible fallback.
1652  * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1653  * different processes are executed:
1655  * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1656  * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1657  * the window.
1659  * - {@link #getSetupProcess} method is called and its result executed
1660  * - {@link #getReadyProcess} method is called and its result executed
1662  * **opened**: The window is now open
1664  * **closing**: The closing stage begins when the window manager's
1665  * {@link OO.ui.WindowManager#closeWindow closeWindow}
1666  * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1668  * - {@link #getHoldProcess} method is called and its result executed
1669  * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1671  * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1672  * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1673  * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1674  * processing can complete. Always assume window processes are executed asynchronously.
1676  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1678  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1680  * @abstract
1681  * @class
1682  * @extends OO.ui.Element
1683  * @mixins OO.EventEmitter
1685  * @constructor
1686  * @param {Object} [config] Configuration options
1687  * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1688  *  `full`.  If omitted, the value of the {@link #static-size static size} property will be used.
1689  */
1690 OO.ui.Window = function OoUiWindow( config ) {
1691         // Configuration initialization
1692         config = config || {};
1694         // Parent constructor
1695         OO.ui.Window.parent.call( this, config );
1697         // Mixin constructors
1698         OO.EventEmitter.call( this );
1700         // Properties
1701         this.manager = null;
1702         this.size = config.size || this.constructor.static.size;
1703         this.$frame = $( '<div>' );
1704         this.$overlay = $( '<div>' );
1705         this.$content = $( '<div>' );
1707         this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
1708         this.$focusTrapAfter = $( '<div>' ).prop( 'tabIndex', 0 );
1709         this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
1711         // Initialization
1712         this.$overlay.addClass( 'oo-ui-window-overlay' );
1713         this.$content
1714                 .addClass( 'oo-ui-window-content' )
1715                 .attr( 'tabindex', 0 );
1716         this.$frame
1717                 .addClass( 'oo-ui-window-frame' )
1718                 .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
1720         this.$element
1721                 .addClass( 'oo-ui-window' )
1722                 .append( this.$frame, this.$overlay );
1724         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1725         // that reference properties not initialized at that time of parent class construction
1726         // TODO: Find a better way to handle post-constructor setup
1727         this.visible = false;
1728         this.$element.addClass( 'oo-ui-element-hidden' );
1731 /* Setup */
1733 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1734 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1736 /* Static Properties */
1739  * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1741  * The static size is used if no #size is configured during construction.
1743  * @static
1744  * @inheritable
1745  * @property {string}
1746  */
1747 OO.ui.Window.static.size = 'medium';
1749 /* Methods */
1752  * Handle mouse down events.
1754  * @private
1755  * @param {jQuery.Event} e Mouse down event
1756  */
1757 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1758         // Prevent clicking on the click-block from stealing focus
1759         if ( e.target === this.$element[ 0 ] ) {
1760                 return false;
1761         }
1765  * Check if the window has been initialized.
1767  * Initialization occurs when a window is added to a manager.
1769  * @return {boolean} Window has been initialized
1770  */
1771 OO.ui.Window.prototype.isInitialized = function () {
1772         return !!this.manager;
1776  * Check if the window is visible.
1778  * @return {boolean} Window is visible
1779  */
1780 OO.ui.Window.prototype.isVisible = function () {
1781         return this.visible;
1785  * Check if the window is opening.
1787  * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
1788  * method.
1790  * @return {boolean} Window is opening
1791  */
1792 OO.ui.Window.prototype.isOpening = function () {
1793         return this.manager.isOpening( this );
1797  * Check if the window is closing.
1799  * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
1801  * @return {boolean} Window is closing
1802  */
1803 OO.ui.Window.prototype.isClosing = function () {
1804         return this.manager.isClosing( this );
1808  * Check if the window is opened.
1810  * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
1812  * @return {boolean} Window is opened
1813  */
1814 OO.ui.Window.prototype.isOpened = function () {
1815         return this.manager.isOpened( this );
1819  * Get the window manager.
1821  * All windows must be attached to a window manager, which is used to open
1822  * and close the window and control its presentation.
1824  * @return {OO.ui.WindowManager} Manager of window
1825  */
1826 OO.ui.Window.prototype.getManager = function () {
1827         return this.manager;
1831  * Get the symbolic name of the window size (e.g., `small` or `medium`).
1833  * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
1834  */
1835 OO.ui.Window.prototype.getSize = function () {
1836         var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ),
1837                 sizes = this.manager.constructor.static.sizes,
1838                 size = this.size;
1840         if ( !sizes[ size ] ) {
1841                 size = this.manager.constructor.static.defaultSize;
1842         }
1843         if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
1844                 size = 'full';
1845         }
1847         return size;
1851  * Get the size properties associated with the current window size
1853  * @return {Object} Size properties
1854  */
1855 OO.ui.Window.prototype.getSizeProperties = function () {
1856         return this.manager.constructor.static.sizes[ this.getSize() ];
1860  * Disable transitions on window's frame for the duration of the callback function, then enable them
1861  * back.
1863  * @private
1864  * @param {Function} callback Function to call while transitions are disabled
1865  */
1866 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
1867         // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1868         // Disable transitions first, otherwise we'll get values from when the window was animating.
1869         // We need to build the transition CSS properties using these specific properties since
1870         // Firefox doesn't return anything useful when asked just for 'transition'.
1871         var oldTransition = this.$frame.css( 'transition-property' ) + ' ' +
1872                 this.$frame.css( 'transition-duration' ) + ' ' +
1873                 this.$frame.css( 'transition-timing-function' ) + ' ' +
1874                 this.$frame.css( 'transition-delay' );
1876         this.$frame.css( 'transition', 'none' );
1877         callback();
1879         // Force reflow to make sure the style changes done inside callback
1880         // really are not transitioned
1881         this.$frame.height();
1882         this.$frame.css( 'transition', oldTransition );
1886  * Get the height of the full window contents (i.e., the window head, body and foot together).
1888  * What consistitutes the head, body, and foot varies depending on the window type.
1889  * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
1890  * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
1891  * and special actions in the head, and dialog content in the body.
1893  * To get just the height of the dialog body, use the #getBodyHeight method.
1895  * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
1896  */
1897 OO.ui.Window.prototype.getContentHeight = function () {
1898         var bodyHeight,
1899                 win = this,
1900                 bodyStyleObj = this.$body[ 0 ].style,
1901                 frameStyleObj = this.$frame[ 0 ].style;
1903         // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1904         // Disable transitions first, otherwise we'll get values from when the window was animating.
1905         this.withoutSizeTransitions( function () {
1906                 var oldHeight = frameStyleObj.height,
1907                         oldPosition = bodyStyleObj.position;
1908                 frameStyleObj.height = '1px';
1909                 // Force body to resize to new width
1910                 bodyStyleObj.position = 'relative';
1911                 bodyHeight = win.getBodyHeight();
1912                 frameStyleObj.height = oldHeight;
1913                 bodyStyleObj.position = oldPosition;
1914         } );
1916         return (
1917                 // Add buffer for border
1918                 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1919                 // Use combined heights of children
1920                 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1921         );
1925  * Get the height of the window body.
1927  * To get the height of the full window contents (the window body, head, and foot together),
1928  * use #getContentHeight.
1930  * When this function is called, the window will temporarily have been resized
1931  * to height=1px, so .scrollHeight measurements can be taken accurately.
1933  * @return {number} Height of the window body in pixels
1934  */
1935 OO.ui.Window.prototype.getBodyHeight = function () {
1936         return this.$body[ 0 ].scrollHeight;
1940  * Get the directionality of the frame (right-to-left or left-to-right).
1942  * @return {string} Directionality: `'ltr'` or `'rtl'`
1943  */
1944 OO.ui.Window.prototype.getDir = function () {
1945         return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
1949  * Get the 'setup' process.
1951  * The setup process is used to set up a window for use in a particular context,
1952  * based on the `data` argument. This method is called during the opening phase of the window’s
1953  * lifecycle.
1955  * Override this method to add additional steps to the ‘setup’ process the parent method provides
1956  * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
1957  * of OO.ui.Process.
1959  * To add window content that persists between openings, you may wish to use the #initialize method
1960  * instead.
1962  * @param {Object} [data] Window opening data
1963  * @return {OO.ui.Process} Setup process
1964  */
1965 OO.ui.Window.prototype.getSetupProcess = function () {
1966         return new OO.ui.Process();
1970  * Get the ‘ready’ process.
1972  * The ready process is used to ready a window for use in a particular
1973  * context, based on the `data` argument. This method is called during the opening phase of
1974  * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
1976  * Override this method to add additional steps to the ‘ready’ process the parent method
1977  * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
1978  * methods of OO.ui.Process.
1980  * @param {Object} [data] Window opening data
1981  * @return {OO.ui.Process} Ready process
1982  */
1983 OO.ui.Window.prototype.getReadyProcess = function () {
1984         return new OO.ui.Process();
1988  * Get the 'hold' process.
1990  * The hold process is used to keep a window from being used in a particular context,
1991  * based on the `data` argument. This method is called during the closing phase of the window’s
1992  * lifecycle.
1994  * Override this method to add additional steps to the 'hold' process the parent method provides
1995  * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
1996  * of OO.ui.Process.
1998  * @param {Object} [data] Window closing data
1999  * @return {OO.ui.Process} Hold process
2000  */
2001 OO.ui.Window.prototype.getHoldProcess = function () {
2002         return new OO.ui.Process();
2006  * Get the ‘teardown’ process.
2008  * The teardown process is used to teardown a window after use. During teardown,
2009  * user interactions within the window are conveyed and the window is closed, based on the `data`
2010  * argument. This method is called during the closing phase of the window’s lifecycle.
2012  * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2013  * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2014  * of OO.ui.Process.
2016  * @param {Object} [data] Window closing data
2017  * @return {OO.ui.Process} Teardown process
2018  */
2019 OO.ui.Window.prototype.getTeardownProcess = function () {
2020         return new OO.ui.Process();
2024  * Set the window manager.
2026  * This will cause the window to initialize. Calling it more than once will cause an error.
2028  * @param {OO.ui.WindowManager} manager Manager for this window
2029  * @throws {Error} An error is thrown if the method is called more than once
2030  * @chainable
2031  */
2032 OO.ui.Window.prototype.setManager = function ( manager ) {
2033         if ( this.manager ) {
2034                 throw new Error( 'Cannot set window manager, window already has a manager' );
2035         }
2037         this.manager = manager;
2038         this.initialize();
2040         return this;
2044  * Set the window size by symbolic name (e.g., 'small' or 'medium')
2046  * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2047  *  `full`
2048  * @chainable
2049  */
2050 OO.ui.Window.prototype.setSize = function ( size ) {
2051         this.size = size;
2052         this.updateSize();
2053         return this;
2057  * Update the window size.
2059  * @throws {Error} An error is thrown if the window is not attached to a window manager
2060  * @chainable
2061  */
2062 OO.ui.Window.prototype.updateSize = function () {
2063         if ( !this.manager ) {
2064                 throw new Error( 'Cannot update window size, must be attached to a manager' );
2065         }
2067         this.manager.updateWindowSize( this );
2069         return this;
2073  * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2074  * when the window is opening. In general, setDimensions should not be called directly.
2076  * To set the size of the window, use the #setSize method.
2078  * @param {Object} dim CSS dimension properties
2079  * @param {string|number} [dim.width] Width
2080  * @param {string|number} [dim.minWidth] Minimum width
2081  * @param {string|number} [dim.maxWidth] Maximum width
2082  * @param {string|number} [dim.height] Height, omit to set based on height of contents
2083  * @param {string|number} [dim.minHeight] Minimum height
2084  * @param {string|number} [dim.maxHeight] Maximum height
2085  * @chainable
2086  */
2087 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2088         var height,
2089                 win = this,
2090                 styleObj = this.$frame[ 0 ].style;
2092         // Calculate the height we need to set using the correct width
2093         if ( dim.height === undefined ) {
2094                 this.withoutSizeTransitions( function () {
2095                         var oldWidth = styleObj.width;
2096                         win.$frame.css( 'width', dim.width || '' );
2097                         height = win.getContentHeight();
2098                         styleObj.width = oldWidth;
2099                 } );
2100         } else {
2101                 height = dim.height;
2102         }
2104         this.$frame.css( {
2105                 width: dim.width || '',
2106                 minWidth: dim.minWidth || '',
2107                 maxWidth: dim.maxWidth || '',
2108                 height: height || '',
2109                 minHeight: dim.minHeight || '',
2110                 maxHeight: dim.maxHeight || ''
2111         } );
2113         return this;
2117  * Initialize window contents.
2119  * Before the window is opened for the first time, #initialize is called so that content that
2120  * persists between openings can be added to the window.
2122  * To set up a window with new content each time the window opens, use #getSetupProcess.
2124  * @throws {Error} An error is thrown if the window is not attached to a window manager
2125  * @chainable
2126  */
2127 OO.ui.Window.prototype.initialize = function () {
2128         if ( !this.manager ) {
2129                 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2130         }
2132         // Properties
2133         this.$head = $( '<div>' );
2134         this.$body = $( '<div>' );
2135         this.$foot = $( '<div>' );
2136         this.$document = $( this.getElementDocument() );
2138         // Events
2139         this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2141         // Initialization
2142         this.$head.addClass( 'oo-ui-window-head' );
2143         this.$body.addClass( 'oo-ui-window-body' );
2144         this.$foot.addClass( 'oo-ui-window-foot' );
2145         this.$content.append( this.$head, this.$body, this.$foot );
2147         return this;
2151  * Called when someone tries to focus the hidden element at the end of the dialog.
2152  * Sends focus back to the start of the dialog.
2154  * @param {jQuery.Event} event Focus event
2155  */
2156 OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
2157         var backwards = this.$focusTrapBefore.is( event.target ),
2158                 element = OO.ui.findFocusable( this.$content, backwards );
2159         if ( element ) {
2160                 // There's a focusable element inside the content, at the front or
2161                 // back depending on which focus trap we hit; select it.
2162                 element.focus();
2163         } else {
2164                 // There's nothing focusable inside the content. As a fallback,
2165                 // this.$content is focusable, and focusing it will keep our focus
2166                 // properly trapped. It's not a *meaningful* focus, since it's just
2167                 // the content-div for the Window, but it's better than letting focus
2168                 // escape into the page.
2169                 this.$content.focus();
2170         }
2174  * Open the window.
2176  * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2177  * method, which returns a promise resolved when the window is done opening.
2179  * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2181  * @param {Object} [data] Window opening data
2182  * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2183  *  if the window fails to open. When the promise is resolved successfully, the first argument of the
2184  *  value is a new promise, which is resolved when the window begins closing.
2185  * @throws {Error} An error is thrown if the window is not attached to a window manager
2186  */
2187 OO.ui.Window.prototype.open = function ( data ) {
2188         if ( !this.manager ) {
2189                 throw new Error( 'Cannot open window, must be attached to a manager' );
2190         }
2192         return this.manager.openWindow( this, data );
2196  * Close the window.
2198  * This method is a wrapper around a call to the window
2199  * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2200  * which returns a closing promise resolved when the window is done closing.
2202  * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2203  * phase of the window’s lifecycle and can be used to specify closing behavior each time
2204  * the window closes.
2206  * @param {Object} [data] Window closing data
2207  * @return {jQuery.Promise} Promise resolved when window is closed
2208  * @throws {Error} An error is thrown if the window is not attached to a window manager
2209  */
2210 OO.ui.Window.prototype.close = function ( data ) {
2211         if ( !this.manager ) {
2212                 throw new Error( 'Cannot close window, must be attached to a manager' );
2213         }
2215         return this.manager.closeWindow( this, data );
2219  * Setup window.
2221  * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2222  * by other systems.
2224  * @param {Object} [data] Window opening data
2225  * @return {jQuery.Promise} Promise resolved when window is setup
2226  */
2227 OO.ui.Window.prototype.setup = function ( data ) {
2228         var win = this;
2230         this.toggle( true );
2232         this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
2233         this.$focusTraps.on( 'focus', this.focusTrapHandler );
2235         return this.getSetupProcess( data ).execute().then( function () {
2236                 // Force redraw by asking the browser to measure the elements' widths
2237                 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2238                 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2239         } );
2243  * Ready window.
2245  * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2246  * by other systems.
2248  * @param {Object} [data] Window opening data
2249  * @return {jQuery.Promise} Promise resolved when window is ready
2250  */
2251 OO.ui.Window.prototype.ready = function ( data ) {
2252         var win = this;
2254         this.$content.focus();
2255         return this.getReadyProcess( data ).execute().then( function () {
2256                 // Force redraw by asking the browser to measure the elements' widths
2257                 win.$element.addClass( 'oo-ui-window-ready' ).width();
2258                 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2259         } );
2263  * Hold window.
2265  * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2266  * by other systems.
2268  * @param {Object} [data] Window closing data
2269  * @return {jQuery.Promise} Promise resolved when window is held
2270  */
2271 OO.ui.Window.prototype.hold = function ( data ) {
2272         var win = this;
2274         return this.getHoldProcess( data ).execute().then( function () {
2275                 // Get the focused element within the window's content
2276                 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2278                 // Blur the focused element
2279                 if ( $focus.length ) {
2280                         $focus[ 0 ].blur();
2281                 }
2283                 // Force redraw by asking the browser to measure the elements' widths
2284                 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2285                 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2286         } );
2290  * Teardown window.
2292  * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2293  * by other systems.
2295  * @param {Object} [data] Window closing data
2296  * @return {jQuery.Promise} Promise resolved when window is torn down
2297  */
2298 OO.ui.Window.prototype.teardown = function ( data ) {
2299         var win = this;
2301         return this.getTeardownProcess( data ).execute().then( function () {
2302                 // Force redraw by asking the browser to measure the elements' widths
2303                 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2304                 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2305                 win.$focusTraps.off( 'focus', win.focusTrapHandler );
2306                 win.toggle( false );
2307         } );
2311  * The Dialog class serves as the base class for the other types of dialogs.
2312  * Unless extended to include controls, the rendered dialog box is a simple window
2313  * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2314  * which opens, closes, and controls the presentation of the window. See the
2315  * [OOjs UI documentation on MediaWiki] [1] for more information.
2317  *     @example
2318  *     // A simple dialog window.
2319  *     function MyDialog( config ) {
2320  *         MyDialog.parent.call( this, config );
2321  *     }
2322  *     OO.inheritClass( MyDialog, OO.ui.Dialog );
2323  *     MyDialog.prototype.initialize = function () {
2324  *         MyDialog.parent.prototype.initialize.call( this );
2325  *         this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2326  *         this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2327  *         this.$body.append( this.content.$element );
2328  *     };
2329  *     MyDialog.prototype.getBodyHeight = function () {
2330  *         return this.content.$element.outerHeight( true );
2331  *     };
2332  *     var myDialog = new MyDialog( {
2333  *         size: 'medium'
2334  *     } );
2335  *     // Create and append a window manager, which opens and closes the window.
2336  *     var windowManager = new OO.ui.WindowManager();
2337  *     $( 'body' ).append( windowManager.$element );
2338  *     windowManager.addWindows( [ myDialog ] );
2339  *     // Open the window!
2340  *     windowManager.openWindow( myDialog );
2342  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2344  * @abstract
2345  * @class
2346  * @extends OO.ui.Window
2347  * @mixins OO.ui.mixin.PendingElement
2349  * @constructor
2350  * @param {Object} [config] Configuration options
2351  */
2352 OO.ui.Dialog = function OoUiDialog( config ) {
2353         // Parent constructor
2354         OO.ui.Dialog.parent.call( this, config );
2356         // Mixin constructors
2357         OO.ui.mixin.PendingElement.call( this );
2359         // Properties
2360         this.actions = new OO.ui.ActionSet();
2361         this.attachedActions = [];
2362         this.currentAction = null;
2363         this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
2365         // Events
2366         this.actions.connect( this, {
2367                 click: 'onActionClick',
2368                 resize: 'onActionResize',
2369                 change: 'onActionsChange'
2370         } );
2372         // Initialization
2373         this.$element
2374                 .addClass( 'oo-ui-dialog' )
2375                 .attr( 'role', 'dialog' );
2378 /* Setup */
2380 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2381 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2383 /* Static Properties */
2386  * Symbolic name of dialog.
2388  * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2389  * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2391  * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2393  * @abstract
2394  * @static
2395  * @inheritable
2396  * @property {string}
2397  */
2398 OO.ui.Dialog.static.name = '';
2401  * The dialog title.
2403  * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
2404  * that will produce a Label node or string. The title can also be specified with data passed to the
2405  * constructor (see #getSetupProcess). In this case, the static value will be overridden.
2407  * @abstract
2408  * @static
2409  * @inheritable
2410  * @property {jQuery|string|Function}
2411  */
2412 OO.ui.Dialog.static.title = '';
2415  * An array of configured {@link OO.ui.ActionWidget action widgets}.
2417  * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2418  * value will be overridden.
2420  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2422  * @static
2423  * @inheritable
2424  * @property {Object[]}
2425  */
2426 OO.ui.Dialog.static.actions = [];
2429  * Close the dialog when the 'Esc' key is pressed.
2431  * @static
2432  * @abstract
2433  * @inheritable
2434  * @property {boolean}
2435  */
2436 OO.ui.Dialog.static.escapable = true;
2438 /* Methods */
2441  * Handle frame document key down events.
2443  * @private
2444  * @param {jQuery.Event} e Key down event
2445  */
2446 OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
2447         var actions;
2448         if ( e.which === OO.ui.Keys.ESCAPE && this.constructor.static.escapable ) {
2449                 this.executeAction( '' );
2450                 e.preventDefault();
2451                 e.stopPropagation();
2452         } else if ( e.which === OO.ui.Keys.ENTER && e.ctrlKey ) {
2453                 actions = this.actions.get( { flags: 'primary', visible: true, disabled: false } );
2454                 if ( actions.length > 0 ) {
2455                         this.executeAction( actions[ 0 ].getAction() );
2456                         e.preventDefault();
2457                         e.stopPropagation();
2458                 }
2459         }
2463  * Handle action resized events.
2465  * @private
2466  * @param {OO.ui.ActionWidget} action Action that was resized
2467  */
2468 OO.ui.Dialog.prototype.onActionResize = function () {
2469         // Override in subclass
2473  * Handle action click events.
2475  * @private
2476  * @param {OO.ui.ActionWidget} action Action that was clicked
2477  */
2478 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2479         if ( !this.isPending() ) {
2480                 this.executeAction( action.getAction() );
2481         }
2485  * Handle actions change event.
2487  * @private
2488  */
2489 OO.ui.Dialog.prototype.onActionsChange = function () {
2490         this.detachActions();
2491         if ( !this.isClosing() ) {
2492                 this.attachActions();
2493         }
2497  * Get the set of actions used by the dialog.
2499  * @return {OO.ui.ActionSet}
2500  */
2501 OO.ui.Dialog.prototype.getActions = function () {
2502         return this.actions;
2506  * Get a process for taking action.
2508  * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2509  * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2510  * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2512  * @param {string} [action] Symbolic name of action
2513  * @return {OO.ui.Process} Action process
2514  */
2515 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2516         return new OO.ui.Process()
2517                 .next( function () {
2518                         if ( !action ) {
2519                                 // An empty action always closes the dialog without data, which should always be
2520                                 // safe and make no changes
2521                                 this.close();
2522                         }
2523                 }, this );
2527  * @inheritdoc
2529  * @param {Object} [data] Dialog opening data
2530  * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2531  *  the {@link #static-title static title}
2532  * @param {Object[]} [data.actions] List of configuration options for each
2533  *   {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2534  */
2535 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2536         data = data || {};
2538         // Parent method
2539         return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
2540                 .next( function () {
2541                         var config = this.constructor.static,
2542                                 actions = data.actions !== undefined ? data.actions : config.actions,
2543                                 title = data.title !== undefined ? data.title : config.title;
2545                         this.title.setLabel( title ).setTitle( title );
2546                         this.actions.add( this.getActionWidgets( actions ) );
2548                         this.$element.on( 'keydown', this.onDialogKeyDownHandler );
2549                 }, this );
2553  * @inheritdoc
2554  */
2555 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2556         // Parent method
2557         return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
2558                 .first( function () {
2559                         this.$element.off( 'keydown', this.onDialogKeyDownHandler );
2561                         this.actions.clear();
2562                         this.currentAction = null;
2563                 }, this );
2567  * @inheritdoc
2568  */
2569 OO.ui.Dialog.prototype.initialize = function () {
2570         var titleId;
2572         // Parent method
2573         OO.ui.Dialog.parent.prototype.initialize.call( this );
2575         titleId = OO.ui.generateElementId();
2577         // Properties
2578         this.title = new OO.ui.LabelWidget( {
2579                 id: titleId
2580         } );
2582         // Initialization
2583         this.$content.addClass( 'oo-ui-dialog-content' );
2584         this.$element.attr( 'aria-labelledby', titleId );
2585         this.setPendingElement( this.$head );
2589  * Get action widgets from a list of configs
2591  * @param {Object[]} actions Action widget configs
2592  * @return {OO.ui.ActionWidget[]} Action widgets
2593  */
2594 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2595         var i, len, widgets = [];
2596         for ( i = 0, len = actions.length; i < len; i++ ) {
2597                 widgets.push(
2598                         new OO.ui.ActionWidget( actions[ i ] )
2599                 );
2600         }
2601         return widgets;
2605  * Attach action actions.
2607  * @protected
2608  */
2609 OO.ui.Dialog.prototype.attachActions = function () {
2610         // Remember the list of potentially attached actions
2611         this.attachedActions = this.actions.get();
2615  * Detach action actions.
2617  * @protected
2618  * @chainable
2619  */
2620 OO.ui.Dialog.prototype.detachActions = function () {
2621         var i, len;
2623         // Detach all actions that may have been previously attached
2624         for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2625                 this.attachedActions[ i ].$element.detach();
2626         }
2627         this.attachedActions = [];
2631  * Execute an action.
2633  * @param {string} action Symbolic name of action to execute
2634  * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2635  */
2636 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2637         this.pushPending();
2638         this.currentAction = action;
2639         return this.getActionProcess( action ).execute()
2640                 .always( this.popPending.bind( this ) );
2644  * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
2645  * consists of a header that contains the dialog title, a body with the message, and a footer that
2646  * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
2647  * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
2649  * There are two basic types of message dialogs, confirmation and alert:
2651  * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
2652  *  more details about the consequences.
2653  * - **alert**: the dialog title describes which event occurred and the message provides more information
2654  *  about why the event occurred.
2656  * The MessageDialog class specifies two actions: ‘accept’, the primary
2657  * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
2658  * passing along the selected action.
2660  * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
2662  *     @example
2663  *     // Example: Creating and opening a message dialog window.
2664  *     var messageDialog = new OO.ui.MessageDialog();
2666  *     // Create and append a window manager.
2667  *     var windowManager = new OO.ui.WindowManager();
2668  *     $( 'body' ).append( windowManager.$element );
2669  *     windowManager.addWindows( [ messageDialog ] );
2670  *     // Open the window.
2671  *     windowManager.openWindow( messageDialog, {
2672  *         title: 'Basic message dialog',
2673  *         message: 'This is the message'
2674  *     } );
2676  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
2678  * @class
2679  * @extends OO.ui.Dialog
2681  * @constructor
2682  * @param {Object} [config] Configuration options
2683  */
2684 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
2685         // Parent constructor
2686         OO.ui.MessageDialog.parent.call( this, config );
2688         // Properties
2689         this.verticalActionLayout = null;
2691         // Initialization
2692         this.$element.addClass( 'oo-ui-messageDialog' );
2695 /* Setup */
2697 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
2699 /* Static Properties */
2701 OO.ui.MessageDialog.static.name = 'message';
2703 OO.ui.MessageDialog.static.size = 'small';
2705 // @deprecated since v0.18.4 as default; TODO: Remove
2706 OO.ui.MessageDialog.static.verbose = true;
2709  * Dialog title.
2711  * The title of a confirmation dialog describes what a progressive action will do. The
2712  * title of an alert dialog describes which event occurred.
2714  * @static
2715  * @inheritable
2716  * @property {jQuery|string|Function|null}
2717  */
2718 OO.ui.MessageDialog.static.title = null;
2721  * The message displayed in the dialog body.
2723  * A confirmation message describes the consequences of a progressive action. An alert
2724  * message describes why an event occurred.
2726  * @static
2727  * @inheritable
2728  * @property {jQuery|string|Function|null}
2729  */
2730 OO.ui.MessageDialog.static.message = null;
2732 // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
2733 OO.ui.MessageDialog.static.actions = [
2734         { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
2735         { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
2738 /* Methods */
2741  * @inheritdoc
2742  */
2743 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
2744         OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
2746         // Events
2747         this.manager.connect( this, {
2748                 resize: 'onResize'
2749         } );
2751         return this;
2755  * @inheritdoc
2756  */
2757 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
2758         this.fitActions();
2759         return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
2763  * Handle window resized events.
2765  * @private
2766  */
2767 OO.ui.MessageDialog.prototype.onResize = function () {
2768         var dialog = this;
2769         dialog.fitActions();
2770         // Wait for CSS transition to finish and do it again :(
2771         setTimeout( function () {
2772                 dialog.fitActions();
2773         }, 300 );
2777  * Toggle action layout between vertical and horizontal.
2779  * @private
2780  * @param {boolean} [value] Layout actions vertically, omit to toggle
2781  * @chainable
2782  */
2783 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
2784         value = value === undefined ? !this.verticalActionLayout : !!value;
2786         if ( value !== this.verticalActionLayout ) {
2787                 this.verticalActionLayout = value;
2788                 this.$actions
2789                         .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
2790                         .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
2791         }
2793         return this;
2797  * @inheritdoc
2798  */
2799 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
2800         if ( action ) {
2801                 return new OO.ui.Process( function () {
2802                         this.close( { action: action } );
2803                 }, this );
2804         }
2805         return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
2809  * @inheritdoc
2811  * @param {Object} [data] Dialog opening data
2812  * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
2813  * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
2814  * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
2815  *   action item
2816  */
2817 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
2818         data = data || {};
2820         // Parent method
2821         return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
2822                 .next( function () {
2823                         this.title.setLabel(
2824                                 data.title !== undefined ? data.title : this.constructor.static.title
2825                         );
2826                         this.message.setLabel(
2827                                 data.message !== undefined ? data.message : this.constructor.static.message
2828                         );
2829                         // @deprecated since v0.18.4 as default; TODO: Remove and make default instead.
2830                         this.message.$element.toggleClass(
2831                                 'oo-ui-messageDialog-message-verbose',
2832                                 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
2833                         );
2834                 }, this );
2838  * @inheritdoc
2839  */
2840 OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
2841         data = data || {};
2843         // Parent method
2844         return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data )
2845                 .next( function () {
2846                         // Focus the primary action button
2847                         var actions = this.actions.get();
2848                         actions = actions.filter( function ( action ) {
2849                                 return action.getFlags().indexOf( 'primary' ) > -1;
2850                         } );
2851                         if ( actions.length > 0 ) {
2852                                 actions[ 0 ].$button.focus();
2853                         }
2854                 }, this );
2858  * @inheritdoc
2859  */
2860 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
2861         var bodyHeight, oldOverflow,
2862                 $scrollable = this.container.$element;
2864         oldOverflow = $scrollable[ 0 ].style.overflow;
2865         $scrollable[ 0 ].style.overflow = 'hidden';
2867         OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
2869         bodyHeight = this.text.$element.outerHeight( true );
2870         $scrollable[ 0 ].style.overflow = oldOverflow;
2872         return bodyHeight;
2876  * @inheritdoc
2877  */
2878 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
2879         var $scrollable = this.container.$element;
2880         OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
2882         // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
2883         // Need to do it after transition completes (250ms), add 50ms just in case.
2884         setTimeout( function () {
2885                 var oldOverflow = $scrollable[ 0 ].style.overflow,
2886                         activeElement = document.activeElement;
2888                 $scrollable[ 0 ].style.overflow = 'hidden';
2890                 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
2892                 // Check reconsiderScrollbars didn't destroy our focus, as we
2893                 // are doing this after the ready process.
2894                 if ( activeElement && activeElement !== document.activeElement && activeElement.focus ) {
2895                         activeElement.focus();
2896                 }
2898                 $scrollable[ 0 ].style.overflow = oldOverflow;
2899         }, 300 );
2901         return this;
2905  * @inheritdoc
2906  */
2907 OO.ui.MessageDialog.prototype.initialize = function () {
2908         // Parent method
2909         OO.ui.MessageDialog.parent.prototype.initialize.call( this );
2911         // Properties
2912         this.$actions = $( '<div>' );
2913         this.container = new OO.ui.PanelLayout( {
2914                 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
2915         } );
2916         this.text = new OO.ui.PanelLayout( {
2917                 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
2918         } );
2919         this.message = new OO.ui.LabelWidget( {
2920                 classes: [ 'oo-ui-messageDialog-message' ]
2921         } );
2923         // Initialization
2924         this.title.$element.addClass( 'oo-ui-messageDialog-title' );
2925         this.$content.addClass( 'oo-ui-messageDialog-content' );
2926         this.container.$element.append( this.text.$element );
2927         this.text.$element.append( this.title.$element, this.message.$element );
2928         this.$body.append( this.container.$element );
2929         this.$actions.addClass( 'oo-ui-messageDialog-actions' );
2930         this.$foot.append( this.$actions );
2934  * @inheritdoc
2935  */
2936 OO.ui.MessageDialog.prototype.attachActions = function () {
2937         var i, len, other, special, others;
2939         // Parent method
2940         OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
2942         special = this.actions.getSpecial();
2943         others = this.actions.getOthers();
2945         if ( special.safe ) {
2946                 this.$actions.append( special.safe.$element );
2947                 special.safe.toggleFramed( false );
2948         }
2949         if ( others.length ) {
2950                 for ( i = 0, len = others.length; i < len; i++ ) {
2951                         other = others[ i ];
2952                         this.$actions.append( other.$element );
2953                         other.toggleFramed( false );
2954                 }
2955         }
2956         if ( special.primary ) {
2957                 this.$actions.append( special.primary.$element );
2958                 special.primary.toggleFramed( false );
2959         }
2961         if ( !this.isOpening() ) {
2962                 // If the dialog is currently opening, this will be called automatically soon.
2963                 // This also calls #fitActions.
2964                 this.updateSize();
2965         }
2969  * Fit action actions into columns or rows.
2971  * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
2973  * @private
2974  */
2975 OO.ui.MessageDialog.prototype.fitActions = function () {
2976         var i, len, action,
2977                 previous = this.verticalActionLayout,
2978                 actions = this.actions.get();
2980         // Detect clipping
2981         this.toggleVerticalActionLayout( false );
2982         for ( i = 0, len = actions.length; i < len; i++ ) {
2983                 action = actions[ i ];
2984                 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
2985                         this.toggleVerticalActionLayout( true );
2986                         break;
2987                 }
2988         }
2990         // Move the body out of the way of the foot
2991         this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
2993         if ( this.verticalActionLayout !== previous ) {
2994                 // We changed the layout, window height might need to be updated.
2995                 this.updateSize();
2996         }
3000  * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
3001  * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
3002  * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
3003  * relevant. The ProcessDialog class is always extended and customized with the actions and content
3004  * required for each process.
3006  * The process dialog box consists of a header that visually represents the ‘working’ state of long
3007  * processes with an animation. The header contains the dialog title as well as
3008  * two {@link OO.ui.ActionWidget action widgets}:  a ‘safe’ action on the left (e.g., ‘Cancel’) and
3009  * a ‘primary’ action on the right (e.g., ‘Done’).
3011  * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
3012  * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
3014  *     @example
3015  *     // Example: Creating and opening a process dialog window.
3016  *     function MyProcessDialog( config ) {
3017  *         MyProcessDialog.parent.call( this, config );
3018  *     }
3019  *     OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
3021  *     MyProcessDialog.static.title = 'Process dialog';
3022  *     MyProcessDialog.static.actions = [
3023  *         { action: 'save', label: 'Done', flags: 'primary' },
3024  *         { label: 'Cancel', flags: 'safe' }
3025  *     ];
3027  *     MyProcessDialog.prototype.initialize = function () {
3028  *         MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
3029  *         this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
3030  *         this.content.$element.append( '<p>This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action)  on the right.</p>' );
3031  *         this.$body.append( this.content.$element );
3032  *     };
3033  *     MyProcessDialog.prototype.getActionProcess = function ( action ) {
3034  *         var dialog = this;
3035  *         if ( action ) {
3036  *             return new OO.ui.Process( function () {
3037  *                 dialog.close( { action: action } );
3038  *             } );
3039  *         }
3040  *         return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
3041  *     };
3043  *     var windowManager = new OO.ui.WindowManager();
3044  *     $( 'body' ).append( windowManager.$element );
3046  *     var dialog = new MyProcessDialog();
3047  *     windowManager.addWindows( [ dialog ] );
3048  *     windowManager.openWindow( dialog );
3050  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
3052  * @abstract
3053  * @class
3054  * @extends OO.ui.Dialog
3056  * @constructor
3057  * @param {Object} [config] Configuration options
3058  */
3059 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
3060         // Parent constructor
3061         OO.ui.ProcessDialog.parent.call( this, config );
3063         // Properties
3064         this.fitOnOpen = false;
3066         // Initialization
3067         this.$element.addClass( 'oo-ui-processDialog' );
3070 /* Setup */
3072 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
3074 /* Methods */
3077  * Handle dismiss button click events.
3079  * Hides errors.
3081  * @private
3082  */
3083 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
3084         this.hideErrors();
3088  * Handle retry button click events.
3090  * Hides errors and then tries again.
3092  * @private
3093  */
3094 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
3095         this.hideErrors();
3096         this.executeAction( this.currentAction );
3100  * @inheritdoc
3101  */
3102 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
3103         if ( this.actions.isSpecial( action ) ) {
3104                 this.fitLabel();
3105         }
3106         return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
3110  * @inheritdoc
3111  */
3112 OO.ui.ProcessDialog.prototype.initialize = function () {
3113         // Parent method
3114         OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
3116         // Properties
3117         this.$navigation = $( '<div>' );
3118         this.$location = $( '<div>' );
3119         this.$safeActions = $( '<div>' );
3120         this.$primaryActions = $( '<div>' );
3121         this.$otherActions = $( '<div>' );
3122         this.dismissButton = new OO.ui.ButtonWidget( {
3123                 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
3124         } );
3125         this.retryButton = new OO.ui.ButtonWidget();
3126         this.$errors = $( '<div>' );
3127         this.$errorsTitle = $( '<div>' );
3129         // Events
3130         this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
3131         this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
3133         // Initialization
3134         this.title.$element.addClass( 'oo-ui-processDialog-title' );
3135         this.$location
3136                 .append( this.title.$element )
3137                 .addClass( 'oo-ui-processDialog-location' );
3138         this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
3139         this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
3140         this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
3141         this.$errorsTitle
3142                 .addClass( 'oo-ui-processDialog-errors-title' )
3143                 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
3144         this.$errors
3145                 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
3146                 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
3147         this.$content
3148                 .addClass( 'oo-ui-processDialog-content' )
3149                 .append( this.$errors );
3150         this.$navigation
3151                 .addClass( 'oo-ui-processDialog-navigation' )
3152                 // Note: Order of appends below is important. These are in the order
3153                 // we want tab to go through them. Display-order is handled entirely
3154                 // by CSS absolute-positioning. As such, primary actions like "done"
3155                 // should go first.
3156                 .append( this.$primaryActions, this.$location, this.$safeActions );
3157         this.$head.append( this.$navigation );
3158         this.$foot.append( this.$otherActions );
3162  * @inheritdoc
3163  */
3164 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
3165         var i, len, config,
3166                 isMobile = OO.ui.isMobile(),
3167                 widgets = [];
3169         for ( i = 0, len = actions.length; i < len; i++ ) {
3170                 config = $.extend( { framed: !OO.ui.isMobile() }, actions[ i ] );
3171                 if ( isMobile &&
3172                         ( config.flags === 'back' || ( Array.isArray( config.flags ) && config.flags.indexOf( 'back' ) !== -1 ) )
3173                 ) {
3174                         $.extend( config, {
3175                                 icon: 'previous',
3176                                 label: ''
3177                         } );
3178                 }
3179                 widgets.push(
3180                         new OO.ui.ActionWidget( config )
3181                 );
3182         }
3183         return widgets;
3187  * @inheritdoc
3188  */
3189 OO.ui.ProcessDialog.prototype.attachActions = function () {
3190         var i, len, other, special, others;
3192         // Parent method
3193         OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
3195         special = this.actions.getSpecial();
3196         others = this.actions.getOthers();
3197         if ( special.primary ) {
3198                 this.$primaryActions.append( special.primary.$element );
3199         }
3200         for ( i = 0, len = others.length; i < len; i++ ) {
3201                 other = others[ i ];
3202                 this.$otherActions.append( other.$element );
3203         }
3204         if ( special.safe ) {
3205                 this.$safeActions.append( special.safe.$element );
3206         }
3208         this.fitLabel();
3209         this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
3213  * @inheritdoc
3214  */
3215 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
3216         var process = this;
3217         return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
3218                 .fail( function ( errors ) {
3219                         process.showErrors( errors || [] );
3220                 } );
3224  * @inheritdoc
3225  */
3226 OO.ui.ProcessDialog.prototype.setDimensions = function () {
3227         // Parent method
3228         OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments );
3230         this.fitLabel();
3234  * Fit label between actions.
3236  * @private
3237  * @chainable
3238  */
3239 OO.ui.ProcessDialog.prototype.fitLabel = function () {
3240         var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth,
3241                 size = this.getSizeProperties();
3243         if ( typeof size.width !== 'number' ) {
3244                 if ( this.isOpened() ) {
3245                         navigationWidth = this.$head.width() - 20;
3246                 } else if ( this.isOpening() ) {
3247                         if ( !this.fitOnOpen ) {
3248                                 // Size is relative and the dialog isn't open yet, so wait.
3249                                 this.manager.opening.done( this.fitLabel.bind( this ) );
3250                                 this.fitOnOpen = true;
3251                         }
3252                         return;
3253                 } else {
3254                         return;
3255                 }
3256         } else {
3257                 navigationWidth = size.width - 20;
3258         }
3260         safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
3261         primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
3262         biggerWidth = Math.max( safeWidth, primaryWidth );
3264         labelWidth = this.title.$element.width();
3266         if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
3267                 // We have enough space to center the label
3268                 leftWidth = rightWidth = biggerWidth;
3269         } else {
3270                 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
3271                 if ( this.getDir() === 'ltr' ) {
3272                         leftWidth = safeWidth;
3273                         rightWidth = primaryWidth;
3274                 } else {
3275                         leftWidth = primaryWidth;
3276                         rightWidth = safeWidth;
3277                 }
3278         }
3280         this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
3282         return this;
3286  * Handle errors that occurred during accept or reject processes.
3288  * @private
3289  * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
3290  */
3291 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
3292         var i, len, $item, actions,
3293                 items = [],
3294                 abilities = {},
3295                 recoverable = true,
3296                 warning = false;
3298         if ( errors instanceof OO.ui.Error ) {
3299                 errors = [ errors ];
3300         }
3302         for ( i = 0, len = errors.length; i < len; i++ ) {
3303                 if ( !errors[ i ].isRecoverable() ) {
3304                         recoverable = false;
3305                 }
3306                 if ( errors[ i ].isWarning() ) {
3307                         warning = true;
3308                 }
3309                 $item = $( '<div>' )
3310                         .addClass( 'oo-ui-processDialog-error' )
3311                         .append( errors[ i ].getMessage() );
3312                 items.push( $item[ 0 ] );
3313         }
3314         this.$errorItems = $( items );
3315         if ( recoverable ) {
3316                 abilities[ this.currentAction ] = true;
3317                 // Copy the flags from the first matching action
3318                 actions = this.actions.get( { actions: this.currentAction } );
3319                 if ( actions.length ) {
3320                         this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
3321                 }
3322         } else {
3323                 abilities[ this.currentAction ] = false;
3324                 this.actions.setAbilities( abilities );
3325         }
3326         if ( warning ) {
3327                 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
3328         } else {
3329                 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
3330         }
3331         this.retryButton.toggle( recoverable );
3332         this.$errorsTitle.after( this.$errorItems );
3333         this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
3337  * Hide errors.
3339  * @private
3340  */
3341 OO.ui.ProcessDialog.prototype.hideErrors = function () {
3342         this.$errors.addClass( 'oo-ui-element-hidden' );
3343         if ( this.$errorItems ) {
3344                 this.$errorItems.remove();
3345                 this.$errorItems = null;
3346         }
3350  * @inheritdoc
3351  */
3352 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
3353         // Parent method
3354         return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
3355                 .first( function () {
3356                         // Make sure to hide errors
3357                         this.hideErrors();
3358                         this.fitOnOpen = false;
3359                 }, this );
3363  * @class OO.ui
3364  */
3367  * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
3368  * OO.ui.confirm.
3370  * @private
3371  * @return {OO.ui.WindowManager}
3372  */
3373 OO.ui.getWindowManager = function () {
3374         if ( !OO.ui.windowManager ) {
3375                 OO.ui.windowManager = new OO.ui.WindowManager();
3376                 $( 'body' ).append( OO.ui.windowManager.$element );
3377                 OO.ui.windowManager.addWindows( {
3378                         messageDialog: new OO.ui.MessageDialog()
3379                 } );
3380         }
3381         return OO.ui.windowManager;
3385  * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
3386  * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
3387  * has only one action button, labelled "OK", clicking it will simply close the dialog.
3389  * A window manager is created automatically when this function is called for the first time.
3391  *     @example
3392  *     OO.ui.alert( 'Something happened!' ).done( function () {
3393  *         console.log( 'User closed the dialog.' );
3394  *     } );
3396  * @param {jQuery|string} text Message text to display
3397  * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3398  * @return {jQuery.Promise} Promise resolved when the user closes the dialog
3399  */
3400 OO.ui.alert = function ( text, options ) {
3401         return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
3402                 message: text,
3403                 actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
3404         }, options ) ).then( function ( opened ) {
3405                 return opened.then( function ( closing ) {
3406                         return closing.then( function () {
3407                                 return $.Deferred().resolve();
3408                         } );
3409                 } );
3410         } );
3414  * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
3415  * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3416  * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
3417  * (labelled "Cancel").
3419  * A window manager is created automatically when this function is called for the first time.
3421  *     @example
3422  *     OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
3423  *         if ( confirmed ) {
3424  *             console.log( 'User clicked "OK"!' );
3425  *         } else {
3426  *             console.log( 'User clicked "Cancel" or closed the dialog.' );
3427  *         }
3428  *     } );
3430  * @param {jQuery|string} text Message text to display
3431  * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3432  * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3433  *  confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
3434  *  `false`.
3435  */
3436 OO.ui.confirm = function ( text, options ) {
3437         return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
3438                 message: text
3439         }, options ) ).then( function ( opened ) {
3440                 return opened.then( function ( closing ) {
3441                         return closing.then( function ( data ) {
3442                                 return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
3443                         } );
3444                 } );
3445         } );
3449  * Display a quick modal prompt dialog, using a OO.ui.MessageDialog. While the dialog is open,
3450  * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3451  * dialog has a text input widget and two action buttons, one to confirm an operation (labelled "OK")
3452  * and one to cancel it (labelled "Cancel").
3454  * A window manager is created automatically when this function is called for the first time.
3456  *     @example
3457  *     OO.ui.prompt( 'Choose a line to go to', { textInput: { placeholder: 'Line number' } } ).done( function ( result ) {
3458  *         if ( result !== null ) {
3459  *             console.log( 'User typed "' + result + '" then clicked "OK".' );
3460  *         } else {
3461  *             console.log( 'User clicked "Cancel" or closed the dialog.' );
3462  *         }
3463  *     } );
3465  * @param {jQuery|string} text Message text to display
3466  * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3467  * @cfg {Object} [textInput] Additional options for text input widget, see OO.ui.TextInputWidget
3468  * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3469  *  confirm, the promise will resolve with the value of the text input widget; otherwise, it will
3470  *  resolve to `null`.
3471  */
3472 OO.ui.prompt = function ( text, options ) {
3473         var manager = OO.ui.getWindowManager(),
3474                 textInput = new OO.ui.TextInputWidget( ( options && options.textInput ) || {} ),
3475                 textField = new OO.ui.FieldLayout( textInput, {
3476                         align: 'top',
3477                         label: text
3478                 } );
3480         // TODO: This is a little hacky, and could be done by extending MessageDialog instead.
3482         return manager.openWindow( 'messageDialog', $.extend( {
3483                 message: textField.$element
3484         }, options ) ).then( function ( opened ) {
3485                 // After ready
3486                 textInput.on( 'enter', function () {
3487                         manager.getCurrentWindow().close( { action: 'accept' } );
3488                 } );
3489                 textInput.focus();
3490                 return opened.then( function ( closing ) {
3491                         return closing.then( function ( data ) {
3492                                 return $.Deferred().resolve( data && data.action === 'accept' ? textInput.getValue() : null );
3493                         } );
3494                 } );
3495         } );
3498 }( OO ) );
3500 //# sourceMappingURL=oojs-ui-windows.js.map