Merge "Exclude redirects from Special:Fewestrevisions"
[lhc/web/wiklou.git] / resources / lib / ooui / oojs-ui-windows.js
1 /*!
2 * OOUI v0.33.3
3 * https://www.mediawiki.org/wiki/OOUI
4 *
5 * Copyright 2011–2019 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2019-07-16T21:33:36Z
10 */
11 ( function ( OO ) {
12
13 'use strict';
14
15 /**
16 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
17 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
18 * of the actions.
19 *
20 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
21 * Please see the [OOUI documentation on MediaWiki] [1] for more information
22 * and examples.
23 *
24 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
25 *
26 * @class
27 * @extends OO.ui.ButtonWidget
28 * @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 );
41
42 // Parent constructor
43 OO.ui.ActionWidget.parent.call( this, config );
44
45 // Mixin constructors
46 OO.ui.mixin.PendingElement.call( this, config );
47
48 // Properties
49 this.action = config.action || '';
50 this.modes = config.modes || [];
51 this.width = 0;
52 this.height = 0;
53
54 // Initialization
55 this.$element.addClass( 'oo-ui-actionWidget' );
56 };
57
58 /* Setup */
59
60 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
61 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
62
63 /* Methods */
64
65 /**
66 * Check if the action is configured to be available in the specified `mode`.
67 *
68 * @param {string} mode Name of mode
69 * @return {boolean} The action is configured with the mode
70 */
71 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
72 return this.modes.indexOf( mode ) !== -1;
73 };
74
75 /**
76 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
77 *
78 * @return {string}
79 */
80 OO.ui.ActionWidget.prototype.getAction = function () {
81 return this.action;
82 };
83
84 /**
85 * Get the symbolic name of the mode or modes for which the action is configured to be available.
86 *
87 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
88 * Only actions that are configured to be available in the current mode will be visible.
89 * All other actions are hidden.
90 *
91 * @return {string[]}
92 */
93 OO.ui.ActionWidget.prototype.getModes = function () {
94 return this.modes.slice();
95 };
96
97 /* eslint-disable no-unused-vars */
98 /**
99 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that
100 * comprise them.
101 * Actions can be made available for specific contexts (modes) and circumstances
102 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
103 *
104 * ActionSets contain two types of actions:
105 *
106 * - Special: Special actions are the first visible actions with special flags, such as 'safe' and
107 * 'primary', the default special flags. Additional special flags can be configured in subclasses
108 * with the static #specialFlags property.
109 * - Other: Other actions include all non-special visible actions.
110 *
111 * See the [OOUI documentation on MediaWiki][1] for more information.
112 *
113 * @example
114 * // Example: An action set used in a process dialog
115 * function MyProcessDialog( config ) {
116 * MyProcessDialog.parent.call( this, config );
117 * }
118 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
119 * MyProcessDialog.static.title = 'An action set in a process dialog';
120 * MyProcessDialog.static.name = 'myProcessDialog';
121 * // An action set that uses modes ('edit' and 'help' mode, in this example).
122 * MyProcessDialog.static.actions = [
123 * {
124 * action: 'continue',
125 * modes: 'edit',
126 * label: 'Continue',
127 * flags: [ 'primary', 'progressive' ]
128 * },
129 * { action: 'help', modes: 'edit', label: 'Help' },
130 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
131 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
132 * ];
133 *
134 * MyProcessDialog.prototype.initialize = function () {
135 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
136 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
137 * this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, ' +
138 * 'cancel, back) configured with modes. This is edit mode. Click \'help\' to see ' +
139 * 'help mode.</p>' );
140 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
141 * this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget ' +
142 * 'is configured to be visible here. Click \'back\' to return to \'edit\' mode.' +
143 * '</p>' );
144 * this.stackLayout = new OO.ui.StackLayout( {
145 * items: [ this.panel1, this.panel2 ]
146 * } );
147 * this.$body.append( this.stackLayout.$element );
148 * };
149 * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
150 * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
151 * .next( function () {
152 * this.actions.setMode( 'edit' );
153 * }, this );
154 * };
155 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
156 * if ( action === 'help' ) {
157 * this.actions.setMode( 'help' );
158 * this.stackLayout.setItem( this.panel2 );
159 * } else if ( action === 'back' ) {
160 * this.actions.setMode( 'edit' );
161 * this.stackLayout.setItem( this.panel1 );
162 * } else if ( action === 'continue' ) {
163 * var dialog = this;
164 * return new OO.ui.Process( function () {
165 * dialog.close();
166 * } );
167 * }
168 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
169 * };
170 * MyProcessDialog.prototype.getBodyHeight = function () {
171 * return this.panel1.$element.outerHeight( true );
172 * };
173 * var windowManager = new OO.ui.WindowManager();
174 * $( document.body ).append( windowManager.$element );
175 * var dialog = new MyProcessDialog( {
176 * size: 'medium'
177 * } );
178 * windowManager.addWindows( [ dialog ] );
179 * windowManager.openWindow( dialog );
180 *
181 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
182 *
183 * @abstract
184 * @class
185 * @mixins OO.EventEmitter
186 *
187 * @constructor
188 * @param {Object} [config] Configuration options
189 */
190 OO.ui.ActionSet = function OoUiActionSet( config ) {
191 // Configuration initialization
192 config = config || {};
193
194 // Mixin constructors
195 OO.EventEmitter.call( this );
196
197 // Properties
198 this.list = [];
199 this.categories = {
200 actions: 'getAction',
201 flags: 'getFlags',
202 modes: 'getModes'
203 };
204 this.categorized = {};
205 this.special = {};
206 this.others = [];
207 this.organized = false;
208 this.changing = false;
209 this.changed = false;
210 };
211 /* eslint-enable no-unused-vars */
212
213 /* Setup */
214
215 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
216
217 /* Static Properties */
218
219 /**
220 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
221 * header of a {@link OO.ui.ProcessDialog process dialog}.
222 * See the [OOUI documentation on MediaWiki][2] for more information and examples.
223 *
224 * [2]:https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs
225 *
226 * @abstract
227 * @static
228 * @inheritable
229 * @property {string}
230 */
231 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
232
233 /* Events */
234
235 /**
236 * @event click
237 *
238 * A 'click' event is emitted when an action is clicked.
239 *
240 * @param {OO.ui.ActionWidget} action Action that was clicked
241 */
242
243 /**
244 * @event add
245 *
246 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
247 *
248 * @param {OO.ui.ActionWidget[]} added Actions added
249 */
250
251 /**
252 * @event remove
253 *
254 * A 'remove' event is emitted when actions are {@link #method-remove removed}
255 * or {@link #clear cleared}.
256 *
257 * @param {OO.ui.ActionWidget[]} added Actions removed
258 */
259
260 /**
261 * @event change
262 *
263 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
264 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode}
265 * is changed.
266 *
267 */
268
269 /* Methods */
270
271 /**
272 * Handle action change events.
273 *
274 * @private
275 * @fires change
276 */
277 OO.ui.ActionSet.prototype.onActionChange = function () {
278 this.organized = false;
279 if ( this.changing ) {
280 this.changed = true;
281 } else {
282 this.emit( 'change' );
283 }
284 };
285
286 /**
287 * Check if an action is one of the special actions.
288 *
289 * @param {OO.ui.ActionWidget} action Action to check
290 * @return {boolean} Action is special
291 */
292 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
293 var flag;
294
295 for ( flag in this.special ) {
296 if ( action === this.special[ flag ] ) {
297 return true;
298 }
299 }
300
301 return false;
302 };
303
304 /**
305 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
306 * or ‘disabled’.
307 *
308 * @param {Object} [filters] Filters to use, omit to get all actions
309 * @param {string|string[]} [filters.actions] Actions that action widgets must have
310 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
311 * @param {string|string[]} [filters.modes] Modes that action widgets must have
312 * @param {boolean} [filters.visible] Action widgets must be visible
313 * @param {boolean} [filters.disabled] Action widgets must be disabled
314 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
315 */
316 OO.ui.ActionSet.prototype.get = function ( filters ) {
317 var i, len, list, category, actions, index, match, matches;
318
319 if ( filters ) {
320 this.organize();
321
322 // Collect category candidates
323 matches = [];
324 for ( category in this.categorized ) {
325 list = filters[ category ];
326 if ( list ) {
327 if ( !Array.isArray( list ) ) {
328 list = [ list ];
329 }
330 for ( i = 0, len = list.length; i < len; i++ ) {
331 actions = this.categorized[ category ][ list[ i ] ];
332 if ( Array.isArray( actions ) ) {
333 matches.push.apply( matches, actions );
334 }
335 }
336 }
337 }
338 // Remove by boolean filters
339 for ( i = 0, len = matches.length; i < len; i++ ) {
340 match = matches[ i ];
341 if (
342 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
343 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
344 ) {
345 matches.splice( i, 1 );
346 len--;
347 i--;
348 }
349 }
350 // Remove duplicates
351 for ( i = 0, len = matches.length; i < len; i++ ) {
352 match = matches[ i ];
353 index = matches.lastIndexOf( match );
354 while ( index !== i ) {
355 matches.splice( index, 1 );
356 len--;
357 index = matches.lastIndexOf( match );
358 }
359 }
360 return matches;
361 }
362 return this.list.slice();
363 };
364
365 /**
366 * Get 'special' actions.
367 *
368 * Special actions are the first visible action widgets with special flags, such as 'safe' and
369 * 'primary'.
370 * Special flags can be configured in subclasses by changing the static #specialFlags property.
371 *
372 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
373 */
374 OO.ui.ActionSet.prototype.getSpecial = function () {
375 this.organize();
376 return $.extend( {}, this.special );
377 };
378
379 /**
380 * Get 'other' actions.
381 *
382 * Other actions include all non-special visible action widgets.
383 *
384 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
385 */
386 OO.ui.ActionSet.prototype.getOthers = function () {
387 this.organize();
388 return this.others.slice();
389 };
390
391 /**
392 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
393 * to be available in the specified mode will be made visible. All other actions will be hidden.
394 *
395 * @param {string} mode The mode. Only actions configured to be available in the specified
396 * mode will be made visible.
397 * @chainable
398 * @return {OO.ui.ActionSet} The widget, for chaining
399 * @fires toggle
400 * @fires change
401 */
402 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
403 var i, len, action;
404
405 this.changing = true;
406 for ( i = 0, len = this.list.length; i < len; i++ ) {
407 action = this.list[ i ];
408 action.toggle( action.hasMode( mode ) );
409 }
410
411 this.organized = false;
412 this.changing = false;
413 this.emit( 'change' );
414
415 return this;
416 };
417
418 /**
419 * Set the abilities of the specified actions.
420 *
421 * Action widgets that are configured with the specified actions will be enabled
422 * or disabled based on the boolean values specified in the `actions`
423 * parameter.
424 *
425 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
426 * values that indicate whether or not the action should be enabled.
427 * @chainable
428 * @return {OO.ui.ActionSet} The widget, for chaining
429 */
430 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
431 var i, len, action, item;
432
433 for ( i = 0, len = this.list.length; i < len; i++ ) {
434 item = this.list[ i ];
435 action = item.getAction();
436 if ( actions[ action ] !== undefined ) {
437 item.setDisabled( !actions[ action ] );
438 }
439 }
440
441 return this;
442 };
443
444 /**
445 * Executes a function once per action.
446 *
447 * When making changes to multiple actions, use this method instead of iterating over the actions
448 * manually to defer emitting a #change event until after all actions have been changed.
449 *
450 * @param {Object|null} filter Filters to use to determine which actions to iterate over; see #get
451 * @param {Function} callback Callback to run for each action; callback is invoked with three
452 * arguments: the action, the action's index, the list of actions being iterated over
453 * @chainable
454 * @return {OO.ui.ActionSet} The widget, for chaining
455 */
456 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
457 this.changed = false;
458 this.changing = true;
459 this.get( filter ).forEach( callback );
460 this.changing = false;
461 if ( this.changed ) {
462 this.emit( 'change' );
463 }
464
465 return this;
466 };
467
468 /**
469 * Add action widgets to the action set.
470 *
471 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
472 * @chainable
473 * @return {OO.ui.ActionSet} The widget, for chaining
474 * @fires add
475 * @fires change
476 */
477 OO.ui.ActionSet.prototype.add = function ( actions ) {
478 var i, len, action;
479
480 this.changing = true;
481 for ( i = 0, len = actions.length; i < len; i++ ) {
482 action = actions[ i ];
483 action.connect( this, {
484 click: [ 'emit', 'click', action ],
485 toggle: [ 'onActionChange' ]
486 } );
487 this.list.push( action );
488 }
489 this.organized = false;
490 this.emit( 'add', actions );
491 this.changing = false;
492 this.emit( 'change' );
493
494 return this;
495 };
496
497 /**
498 * Remove action widgets from the set.
499 *
500 * To remove all actions, you may wish to use the #clear method instead.
501 *
502 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
503 * @chainable
504 * @return {OO.ui.ActionSet} The widget, for chaining
505 * @fires remove
506 * @fires change
507 */
508 OO.ui.ActionSet.prototype.remove = function ( actions ) {
509 var i, len, index, action;
510
511 this.changing = true;
512 for ( i = 0, len = actions.length; i < len; i++ ) {
513 action = actions[ i ];
514 index = this.list.indexOf( action );
515 if ( index !== -1 ) {
516 action.disconnect( this );
517 this.list.splice( index, 1 );
518 }
519 }
520 this.organized = false;
521 this.emit( 'remove', actions );
522 this.changing = false;
523 this.emit( 'change' );
524
525 return this;
526 };
527
528 /**
529 * Remove all action widgets from the set.
530 *
531 * To remove only specified actions, use the {@link #method-remove remove} method instead.
532 *
533 * @chainable
534 * @return {OO.ui.ActionSet} The widget, for chaining
535 * @fires remove
536 * @fires change
537 */
538 OO.ui.ActionSet.prototype.clear = function () {
539 var i, len, action,
540 removed = this.list.slice();
541
542 this.changing = true;
543 for ( i = 0, len = this.list.length; i < len; i++ ) {
544 action = this.list[ i ];
545 action.disconnect( this );
546 }
547
548 this.list = [];
549
550 this.organized = false;
551 this.emit( 'remove', removed );
552 this.changing = false;
553 this.emit( 'change' );
554
555 return this;
556 };
557
558 /**
559 * Organize actions.
560 *
561 * This is called whenever organized information is requested. It will only reorganize the actions
562 * if something has changed since the last time it ran.
563 *
564 * @private
565 * @chainable
566 * @return {OO.ui.ActionSet} The widget, for chaining
567 */
568 OO.ui.ActionSet.prototype.organize = function () {
569 var i, iLen, j, jLen, flag, action, category, list, item, special,
570 specialFlags = this.constructor.static.specialFlags;
571
572 if ( !this.organized ) {
573 this.categorized = {};
574 this.special = {};
575 this.others = [];
576 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
577 action = this.list[ i ];
578 if ( action.isVisible() ) {
579 // Populate categories
580 for ( category in this.categories ) {
581 if ( !this.categorized[ category ] ) {
582 this.categorized[ category ] = {};
583 }
584 list = action[ this.categories[ category ] ]();
585 if ( !Array.isArray( list ) ) {
586 list = [ list ];
587 }
588 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
589 item = list[ j ];
590 if ( !this.categorized[ category ][ item ] ) {
591 this.categorized[ category ][ item ] = [];
592 }
593 this.categorized[ category ][ item ].push( action );
594 }
595 }
596 // Populate special/others
597 special = false;
598 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
599 flag = specialFlags[ j ];
600 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
601 this.special[ flag ] = action;
602 special = true;
603 break;
604 }
605 }
606 if ( !special ) {
607 this.others.push( action );
608 }
609 }
610 }
611 this.organized = true;
612 }
613
614 return this;
615 };
616
617 /**
618 * Errors contain a required message (either a string or jQuery selection) that is used to describe
619 * what went wrong in a {@link OO.ui.Process process}. The error's #recoverable and #warning
620 * configurations are used to customize the appearance and functionality of the error interface.
621 *
622 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss'
623 * and 'Try again' (i.e., the error is 'recoverable' by default). If the error is not recoverable,
624 * the 'Try again' button will not be rendered and the widget that initiated the failed process will
625 * be disabled.
626 *
627 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button,
628 * which will try the process again.
629 *
630 * For an example of error interfaces, please see the [OOUI documentation on MediaWiki][1].
631 *
632 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Processes_and_errors
633 *
634 * @class
635 *
636 * @constructor
637 * @param {string|jQuery} message Description of error
638 * @param {Object} [config] Configuration options
639 * @cfg {boolean} [recoverable=true] Error is recoverable.
640 * By default, errors are recoverable, and users can try the process again.
641 * @cfg {boolean} [warning=false] Error is a warning.
642 * If the error is a warning, the error interface will include a
643 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the
644 * warning is not triggered a second time if the user chooses to continue.
645 */
646 OO.ui.Error = function OoUiError( message, config ) {
647 // Allow passing positional parameters inside the config object
648 if ( OO.isPlainObject( message ) && config === undefined ) {
649 config = message;
650 message = config.message;
651 }
652
653 // Configuration initialization
654 config = config || {};
655
656 // Properties
657 this.message = message instanceof $ ? message : String( message );
658 this.recoverable = config.recoverable === undefined || !!config.recoverable;
659 this.warning = !!config.warning;
660 };
661
662 /* Setup */
663
664 OO.initClass( OO.ui.Error );
665
666 /* Methods */
667
668 /**
669 * Check if the error is recoverable.
670 *
671 * If the error is recoverable, users are able to try the process again.
672 *
673 * @return {boolean} Error is recoverable
674 */
675 OO.ui.Error.prototype.isRecoverable = function () {
676 return this.recoverable;
677 };
678
679 /**
680 * Check if the error is a warning.
681 *
682 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
683 *
684 * @return {boolean} Error is warning
685 */
686 OO.ui.Error.prototype.isWarning = function () {
687 return this.warning;
688 };
689
690 /**
691 * Get error message as DOM nodes.
692 *
693 * @return {jQuery} Error message in DOM nodes
694 */
695 OO.ui.Error.prototype.getMessage = function () {
696 return this.message instanceof $ ?
697 this.message.clone() :
698 $( '<div>' ).text( this.message ).contents();
699 };
700
701 /**
702 * Get the error message text.
703 *
704 * @return {string} Error message
705 */
706 OO.ui.Error.prototype.getMessageText = function () {
707 return this.message instanceof $ ? this.message.text() : this.message;
708 };
709
710 /**
711 * A Process is a list of steps that are called in sequence. The step can be a number, a
712 * jQuery promise, or a function:
713 *
714 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
715 * - **promise**: the process will continue to the next step when the promise is successfully
716 * resolved or stop if the promise is rejected.
717 * - **function**: the process will execute the function. The process will stop if the function
718 * returns either a boolean `false` or a promise that is rejected; if the function returns a
719 * number, the process will wait for that number of milliseconds before proceeding.
720 *
721 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
722 * configured, users can dismiss the error and try the process again, or not. If a process is
723 * stopped, its remaining steps will not be performed.
724 *
725 * @class
726 *
727 * @constructor
728 * @param {number|jQuery.Promise|Function} step Number of milliseconds to wait before proceeding,
729 * promise that must be resolved before proceeding, or a function to execute. See #createStep for
730 * more information. See #createStep for more information.
731 * @param {Object} [context=null] Execution context of the function. The context is ignored if the
732 * step is a number or promise.
733 */
734 OO.ui.Process = function ( step, context ) {
735 // Properties
736 this.steps = [];
737
738 // Initialization
739 if ( step !== undefined ) {
740 this.next( step, context );
741 }
742 };
743
744 /* Setup */
745
746 OO.initClass( OO.ui.Process );
747
748 /* Methods */
749
750 /**
751 * Start the process.
752 *
753 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
754 * If any of the steps return a promise that is rejected or a boolean false, this promise is
755 * rejected and any remaining steps are not performed.
756 */
757 OO.ui.Process.prototype.execute = function () {
758 var i, len, promise;
759
760 /**
761 * Continue execution.
762 *
763 * @ignore
764 * @param {Array} step A function and the context it should be called in
765 * @return {Function} Function that continues the process
766 */
767 function proceed( step ) {
768 return function () {
769 // Execute step in the correct context
770 var deferred,
771 result = step.callback.call( step.context );
772
773 if ( result === false ) {
774 // Use rejected promise for boolean false results
775 return $.Deferred().reject( [] ).promise();
776 }
777 if ( typeof result === 'number' ) {
778 if ( result < 0 ) {
779 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
780 }
781 // Use a delayed promise for numbers, expecting them to be in milliseconds
782 deferred = $.Deferred();
783 setTimeout( deferred.resolve, result );
784 return deferred.promise();
785 }
786 if ( result instanceof OO.ui.Error ) {
787 // Use rejected promise for error
788 return $.Deferred().reject( [ result ] ).promise();
789 }
790 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
791 // Use rejected promise for list of errors
792 return $.Deferred().reject( result ).promise();
793 }
794 // Duck-type the object to see if it can produce a promise
795 if ( result && typeof result.promise === 'function' ) {
796 // Use a promise generated from the result
797 return result.promise();
798 }
799 // Use resolved promise for other results
800 return $.Deferred().resolve().promise();
801 };
802 }
803
804 if ( this.steps.length ) {
805 // Generate a chain reaction of promises
806 promise = proceed( this.steps[ 0 ] )();
807 for ( i = 1, len = this.steps.length; i < len; i++ ) {
808 promise = promise.then( proceed( this.steps[ i ] ) );
809 }
810 } else {
811 promise = $.Deferred().resolve().promise();
812 }
813
814 return promise;
815 };
816
817 /**
818 * Create a process step.
819 *
820 * @private
821 * @param {number|jQuery.Promise|Function} step
822 *
823 * - Number of milliseconds to wait before proceeding
824 * - Promise that must be resolved before proceeding
825 * - Function to execute
826 * - If the function returns a boolean false the process will stop
827 * - If the function returns a promise, the process will continue to the next
828 * step when the promise is resolved or stop if the promise is rejected
829 * - If the function returns a number, the process will wait for that number of
830 * milliseconds before proceeding
831 * @param {Object} [context=null] Execution context of the function. The context is
832 * ignored if the step is a number or promise.
833 * @return {Object} Step object, with `callback` and `context` properties
834 */
835 OO.ui.Process.prototype.createStep = function ( step, context ) {
836 if ( typeof step === 'number' || typeof step.promise === 'function' ) {
837 return {
838 callback: function () {
839 return step;
840 },
841 context: null
842 };
843 }
844 if ( typeof step === 'function' ) {
845 return {
846 callback: step,
847 context: context
848 };
849 }
850 throw new Error( 'Cannot create process step: number, promise or function expected' );
851 };
852
853 /**
854 * Add step to the beginning of the process.
855 *
856 * @inheritdoc #createStep
857 * @return {OO.ui.Process} this
858 * @chainable
859 */
860 OO.ui.Process.prototype.first = function ( step, context ) {
861 this.steps.unshift( this.createStep( step, context ) );
862 return this;
863 };
864
865 /**
866 * Add step to the end of the process.
867 *
868 * @inheritdoc #createStep
869 * @return {OO.ui.Process} this
870 * @chainable
871 */
872 OO.ui.Process.prototype.next = function ( step, context ) {
873 this.steps.push( this.createStep( step, context ) );
874 return this;
875 };
876
877 /**
878 * A window instance represents the life cycle for one single opening of a window
879 * until its closing.
880 *
881 * While OO.ui.WindowManager will reuse OO.ui.Window objects, each time a window is
882 * opened, a new lifecycle starts.
883 *
884 * For more information, please see the [OOUI documentation on MediaWiki] [1].
885 *
886 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows
887 *
888 * @class
889 *
890 * @constructor
891 */
892 OO.ui.WindowInstance = function OoUiWindowInstance() {
893 var deferreds = {
894 opening: $.Deferred(),
895 opened: $.Deferred(),
896 closing: $.Deferred(),
897 closed: $.Deferred()
898 };
899
900 /**
901 * @private
902 * @property {Object}
903 */
904 this.deferreds = deferreds;
905
906 // Set these up as chained promises so that rejecting of
907 // an earlier stage automatically rejects the subsequent
908 // would-be stages as well.
909
910 /**
911 * @property {jQuery.Promise}
912 */
913 this.opening = deferreds.opening.promise();
914 /**
915 * @property {jQuery.Promise}
916 */
917 this.opened = this.opening.then( function () {
918 return deferreds.opened;
919 } );
920 /**
921 * @property {jQuery.Promise}
922 */
923 this.closing = this.opened.then( function () {
924 return deferreds.closing;
925 } );
926 /**
927 * @property {jQuery.Promise}
928 */
929 this.closed = this.closing.then( function () {
930 return deferreds.closed;
931 } );
932 };
933
934 /* Setup */
935
936 OO.initClass( OO.ui.WindowInstance );
937
938 /**
939 * Check if window is opening.
940 *
941 * @return {boolean} Window is opening
942 */
943 OO.ui.WindowInstance.prototype.isOpening = function () {
944 return this.deferreds.opened.state() === 'pending';
945 };
946
947 /**
948 * Check if window is opened.
949 *
950 * @return {boolean} Window is opened
951 */
952 OO.ui.WindowInstance.prototype.isOpened = function () {
953 return this.deferreds.opened.state() === 'resolved' &&
954 this.deferreds.closing.state() === 'pending';
955 };
956
957 /**
958 * Check if window is closing.
959 *
960 * @return {boolean} Window is closing
961 */
962 OO.ui.WindowInstance.prototype.isClosing = function () {
963 return this.deferreds.closing.state() === 'resolved' &&
964 this.deferreds.closed.state() === 'pending';
965 };
966
967 /**
968 * Check if window is closed.
969 *
970 * @return {boolean} Window is closed
971 */
972 OO.ui.WindowInstance.prototype.isClosed = function () {
973 return this.deferreds.closed.state() === 'resolved';
974 };
975
976 /**
977 * Window managers are used to open and close {@link OO.ui.Window windows} and control their
978 * presentation. Managed windows are mutually exclusive. If a new window is opened while a current
979 * window is opening or is opened, the current window will be closed and any on-going
980 * {@link OO.ui.Process process} will be cancelled. Windows
981 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
982 * pertinent data and reused.
983 *
984 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
985 * `opened`, and `closing`, which represent the primary stages of the cycle:
986 *
987 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
988 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
989 *
990 * - an `opening` event is emitted with an `opening` promise
991 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution
992 * before the window’s {@link OO.ui.Window#method-setup setup} method is called which executes
993 * OO.ui.Window#getSetupProcess.
994 * - a `setup` progress notification is emitted from the `opening` promise
995 * - the #getReadyDelay method is called the returned value is used to time a pause in execution
996 * before the window’s {@link OO.ui.Window#method-ready ready} method is called which executes
997 * OO.ui.Window#getReadyProcess.
998 * - a `ready` progress notification is emitted from the `opening` promise
999 * - the `opening` promise is resolved with an `opened` promise
1000 *
1001 * **Opened**: the window is now open.
1002 *
1003 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
1004 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
1005 * to close the window.
1006 *
1007 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
1008 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution
1009 * before the window's {@link OO.ui.Window#getHoldProcess getHoldProcess} method is called on the
1010 * window and its result executed
1011 * - a `hold` progress notification is emitted from the `closing` promise
1012 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in
1013 * execution before the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method
1014 * is called on the window and its result executed
1015 * - a `teardown` progress notification is emitted from the `closing` promise
1016 * - the `closing` promise is resolved. The window is now closed
1017 *
1018 * See the [OOUI documentation on MediaWiki][1] for more information.
1019 *
1020 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
1021 *
1022 * @class
1023 * @extends OO.ui.Element
1024 * @mixins OO.EventEmitter
1025 *
1026 * @constructor
1027 * @param {Object} [config] Configuration options
1028 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
1029 * Note that window classes that are instantiated with a factory must have
1030 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
1031 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
1032 */
1033 OO.ui.WindowManager = function OoUiWindowManager( config ) {
1034 // Configuration initialization
1035 config = config || {};
1036
1037 // Parent constructor
1038 OO.ui.WindowManager.parent.call( this, config );
1039
1040 // Mixin constructors
1041 OO.EventEmitter.call( this );
1042
1043 // Properties
1044 this.factory = config.factory;
1045 this.modal = config.modal === undefined || !!config.modal;
1046 this.windows = {};
1047 // Deprecated placeholder promise given to compatOpening in openWindow()
1048 // that is resolved in closeWindow().
1049 this.compatOpened = null;
1050 this.preparingToOpen = null;
1051 this.preparingToClose = null;
1052 this.currentWindow = null;
1053 this.globalEvents = false;
1054 this.$returnFocusTo = null;
1055 this.$ariaHidden = null;
1056 this.onWindowResizeTimeout = null;
1057 this.onWindowResizeHandler = this.onWindowResize.bind( this );
1058 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
1059
1060 // Initialization
1061 this.$element
1062 .addClass( 'oo-ui-windowManager' )
1063 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
1064 if ( this.modal ) {
1065 this.$element.attr( 'aria-hidden', true );
1066 }
1067 };
1068
1069 /* Setup */
1070
1071 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
1072 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
1073
1074 /* Events */
1075
1076 /**
1077 * An 'opening' event is emitted when the window begins to be opened.
1078 *
1079 * @event opening
1080 * @param {OO.ui.Window} win Window that's being opened
1081 * @param {jQuery.Promise} opened A promise resolved with a value when the window is opened
1082 * successfully. This promise also emits `setup` and `ready` notifications. When this promise is
1083 * resolved, the first argument of the value is an 'closed' promise, the second argument is the
1084 * opening data.
1085 * @param {Object} data Window opening data
1086 */
1087
1088 /**
1089 * A 'closing' event is emitted when the window begins to be closed.
1090 *
1091 * @event closing
1092 * @param {OO.ui.Window} win Window that's being closed
1093 * @param {jQuery.Promise} closed A promise resolved with a value when the window is closed
1094 * successfully. This promise also emits `hold` and `teardown` notifications. When this promise is
1095 * resolved, the first argument of its value is the closing data.
1096 * @param {Object} data Window closing data
1097 */
1098
1099 /**
1100 * A 'resize' event is emitted when a window is resized.
1101 *
1102 * @event resize
1103 * @param {OO.ui.Window} win Window that was resized
1104 */
1105
1106 /* Static Properties */
1107
1108 /**
1109 * Map of the symbolic name of each window size and its CSS properties.
1110 *
1111 * @static
1112 * @inheritable
1113 * @property {Object}
1114 */
1115 OO.ui.WindowManager.static.sizes = {
1116 small: {
1117 width: 300
1118 },
1119 medium: {
1120 width: 500
1121 },
1122 large: {
1123 width: 700
1124 },
1125 larger: {
1126 width: 900
1127 },
1128 full: {
1129 // These can be non-numeric because they are never used in calculations
1130 width: '100%',
1131 height: '100%'
1132 }
1133 };
1134
1135 /**
1136 * Symbolic name of the default window size.
1137 *
1138 * The default size is used if the window's requested size is not recognized.
1139 *
1140 * @static
1141 * @inheritable
1142 * @property {string}
1143 */
1144 OO.ui.WindowManager.static.defaultSize = 'medium';
1145
1146 /* Methods */
1147
1148 /**
1149 * Handle window resize events.
1150 *
1151 * @private
1152 * @param {jQuery.Event} e Window resize event
1153 */
1154 OO.ui.WindowManager.prototype.onWindowResize = function () {
1155 clearTimeout( this.onWindowResizeTimeout );
1156 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
1157 };
1158
1159 /**
1160 * Handle window resize events.
1161 *
1162 * @private
1163 * @param {jQuery.Event} e Window resize event
1164 */
1165 OO.ui.WindowManager.prototype.afterWindowResize = function () {
1166 var currentFocusedElement = document.activeElement;
1167 if ( this.currentWindow ) {
1168 this.updateWindowSize( this.currentWindow );
1169
1170 // Restore focus to the original element if it has changed.
1171 // When a layout change is made on resize inputs lose focus
1172 // on Android (Chrome and Firefox), see T162127.
1173 if ( currentFocusedElement !== document.activeElement ) {
1174 currentFocusedElement.focus();
1175 }
1176 }
1177 };
1178
1179 /**
1180 * Check if window is opening.
1181 *
1182 * @param {OO.ui.Window} win Window to check
1183 * @return {boolean} Window is opening
1184 */
1185 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
1186 return win === this.currentWindow && !!this.lifecycle &&
1187 this.lifecycle.isOpening();
1188 };
1189
1190 /**
1191 * Check if window is closing.
1192 *
1193 * @param {OO.ui.Window} win Window to check
1194 * @return {boolean} Window is closing
1195 */
1196 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
1197 return win === this.currentWindow && !!this.lifecycle &&
1198 this.lifecycle.isClosing();
1199 };
1200
1201 /**
1202 * Check if window is opened.
1203 *
1204 * @param {OO.ui.Window} win Window to check
1205 * @return {boolean} Window is opened
1206 */
1207 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
1208 return win === this.currentWindow && !!this.lifecycle &&
1209 this.lifecycle.isOpened();
1210 };
1211
1212 /**
1213 * Check if a window is being managed.
1214 *
1215 * @param {OO.ui.Window} win Window to check
1216 * @return {boolean} Window is being managed
1217 */
1218 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
1219 var name;
1220
1221 for ( name in this.windows ) {
1222 if ( this.windows[ name ] === win ) {
1223 return true;
1224 }
1225 }
1226
1227 return false;
1228 };
1229
1230 /**
1231 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
1232 *
1233 * @param {OO.ui.Window} win Window being opened
1234 * @param {Object} [data] Window opening data
1235 * @return {number} Milliseconds to wait
1236 */
1237 OO.ui.WindowManager.prototype.getSetupDelay = function () {
1238 return 0;
1239 };
1240
1241 /**
1242 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’
1243 * process.
1244 *
1245 * @param {OO.ui.Window} win Window being opened
1246 * @param {Object} [data] Window opening data
1247 * @return {number} Milliseconds to wait
1248 */
1249 OO.ui.WindowManager.prototype.getReadyDelay = function () {
1250 return this.modal ? OO.ui.theme.getDialogTransitionDuration() : 0;
1251 };
1252
1253 /**
1254 * Get the number of milliseconds to wait after closing has begun before executing the 'hold'
1255 * process.
1256 *
1257 * @param {OO.ui.Window} win Window being closed
1258 * @param {Object} [data] Window closing data
1259 * @return {number} Milliseconds to wait
1260 */
1261 OO.ui.WindowManager.prototype.getHoldDelay = function () {
1262 return 0;
1263 };
1264
1265 /**
1266 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
1267 * executing the ‘teardown’ process.
1268 *
1269 * @param {OO.ui.Window} win Window being closed
1270 * @param {Object} [data] Window closing data
1271 * @return {number} Milliseconds to wait
1272 */
1273 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
1274 return this.modal ? OO.ui.theme.getDialogTransitionDuration() : 0;
1275 };
1276
1277 /**
1278 * Get a window by its symbolic name.
1279 *
1280 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will
1281 * be instantiated and added to the window manager automatically. Please see the [OOUI documentation
1282 * on MediaWiki][3] for more information about using factories.
1283 * [3]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
1284 *
1285 * @param {string} name Symbolic name of the window
1286 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
1287 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
1288 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
1289 */
1290 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
1291 var deferred = $.Deferred(),
1292 win = this.windows[ name ];
1293
1294 if ( !( win instanceof OO.ui.Window ) ) {
1295 if ( this.factory ) {
1296 if ( !this.factory.lookup( name ) ) {
1297 deferred.reject( new OO.ui.Error(
1298 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
1299 ) );
1300 } else {
1301 win = this.factory.create( name );
1302 this.addWindows( [ win ] );
1303 deferred.resolve( win );
1304 }
1305 } else {
1306 deferred.reject( new OO.ui.Error(
1307 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
1308 ) );
1309 }
1310 } else {
1311 deferred.resolve( win );
1312 }
1313
1314 return deferred.promise();
1315 };
1316
1317 /**
1318 * Get current window.
1319 *
1320 * @return {OO.ui.Window|null} Currently opening/opened/closing window
1321 */
1322 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
1323 return this.currentWindow;
1324 };
1325
1326 /* eslint-disable valid-jsdoc */
1327 /**
1328 * Open a window.
1329 *
1330 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
1331 * @param {Object} [data] Window opening data
1332 * @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when
1333 * closed. Defaults the current activeElement. If set to null, focus isn't changed on close.
1334 * @return {OO.ui.WindowInstance} A lifecycle object representing this particular
1335 * opening of the window. For backwards-compatibility, then object is also a Thenable that is
1336 * resolved when the window is done opening, with nested promise for when closing starts. This
1337 * behaviour is deprecated and is not compatible with jQuery 3, see T163510.
1338 * @fires opening
1339 */
1340 OO.ui.WindowManager.prototype.openWindow = function ( win, data, lifecycle, compatOpening ) {
1341 /* eslint-enable valid-jsdoc */
1342 var error,
1343 manager = this;
1344 data = data || {};
1345
1346 // Internal parameter 'lifecycle' allows this method to always return
1347 // a lifecycle even if the window still needs to be created
1348 // asynchronously when 'win' is a string.
1349 lifecycle = lifecycle || new OO.ui.WindowInstance();
1350 compatOpening = compatOpening || $.Deferred();
1351
1352 // Turn lifecycle into a Thenable for backwards-compatibility with
1353 // the deprecated nested-promise behaviour, see T163510.
1354 [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
1355 .forEach( function ( method ) {
1356 lifecycle[ method ] = function () {
1357 OO.ui.warnDeprecation(
1358 'Using the return value of openWindow as a promise is deprecated. ' +
1359 'Use .openWindow( ... ).opening.' + method + '( ... ) instead.'
1360 );
1361 return compatOpening[ method ].apply( this, arguments );
1362 };
1363 } );
1364
1365 // Argument handling
1366 if ( typeof win === 'string' ) {
1367 this.getWindow( win ).then(
1368 function ( win ) {
1369 manager.openWindow( win, data, lifecycle, compatOpening );
1370 },
1371 function ( err ) {
1372 lifecycle.deferreds.opening.reject( err );
1373 }
1374 );
1375 return lifecycle;
1376 }
1377
1378 // Error handling
1379 if ( !this.hasWindow( win ) ) {
1380 error = 'Cannot open window: window is not attached to manager';
1381 } else if ( this.lifecycle && this.lifecycle.isOpened() ) {
1382 error = 'Cannot open window: another window is open';
1383 } else if ( this.preparingToOpen || ( this.lifecycle && this.lifecycle.isOpening() ) ) {
1384 error = 'Cannot open window: another window is opening';
1385 }
1386
1387 if ( error ) {
1388 compatOpening.reject( new OO.ui.Error( error ) );
1389 lifecycle.deferreds.opening.reject( new OO.ui.Error( error ) );
1390 return lifecycle;
1391 }
1392
1393 // If a window is currently closing, wait for it to complete
1394 this.preparingToOpen = $.when( this.lifecycle && this.lifecycle.closed );
1395 // Ensure handlers get called after preparingToOpen is set
1396 this.preparingToOpen.done( function () {
1397 if ( manager.modal ) {
1398 manager.toggleGlobalEvents( true );
1399 manager.toggleAriaIsolation( true );
1400 }
1401 manager.$returnFocusTo = data.$returnFocusTo !== undefined ?
1402 data.$returnFocusTo :
1403 $( document.activeElement );
1404 manager.currentWindow = win;
1405 manager.lifecycle = lifecycle;
1406 manager.preparingToOpen = null;
1407 manager.emit( 'opening', win, compatOpening, data );
1408 lifecycle.deferreds.opening.resolve( data );
1409 setTimeout( function () {
1410 manager.compatOpened = $.Deferred();
1411 win.setup( data ).then( function () {
1412 compatOpening.notify( { state: 'setup' } );
1413 setTimeout( function () {
1414 win.ready( data ).then( function () {
1415 compatOpening.notify( { state: 'ready' } );
1416 lifecycle.deferreds.opened.resolve( data );
1417 compatOpening.resolve( manager.compatOpened.promise(), data );
1418 manager.togglePreventIosScrolling( true );
1419 }, function () {
1420 lifecycle.deferreds.opened.reject();
1421 compatOpening.reject();
1422 manager.closeWindow( win );
1423 } );
1424 }, manager.getReadyDelay() );
1425 }, function () {
1426 lifecycle.deferreds.opened.reject();
1427 compatOpening.reject();
1428 manager.closeWindow( win );
1429 } );
1430 }, manager.getSetupDelay() );
1431 } );
1432
1433 return lifecycle;
1434 };
1435
1436 /**
1437 * Close a window.
1438 *
1439 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
1440 * @param {Object} [data] Window closing data
1441 * @return {OO.ui.WindowInstance} A lifecycle object representing this particular
1442 * opening of the window. For backwards-compatibility, the object is also a Thenable that is
1443 * resolved when the window is done closing, see T163510.
1444 * @fires closing
1445 */
1446 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
1447 var error,
1448 manager = this,
1449 compatClosing = $.Deferred(),
1450 lifecycle = this.lifecycle,
1451 compatOpened;
1452
1453 // Argument handling
1454 if ( typeof win === 'string' ) {
1455 win = this.windows[ win ];
1456 } else if ( !this.hasWindow( win ) ) {
1457 win = null;
1458 }
1459
1460 // Error handling
1461 if ( !lifecycle ) {
1462 error = 'Cannot close window: no window is currently open';
1463 } else if ( !win ) {
1464 error = 'Cannot close window: window is not attached to manager';
1465 } else if ( win !== this.currentWindow || this.lifecycle.isClosed() ) {
1466 error = 'Cannot close window: window already closed with different data';
1467 } else if ( this.preparingToClose || this.lifecycle.isClosing() ) {
1468 error = 'Cannot close window: window already closing with different data';
1469 }
1470
1471 if ( error ) {
1472 // This function was called for the wrong window and we don't want to mess with the current
1473 // window's state.
1474 lifecycle = new OO.ui.WindowInstance();
1475 // Pretend the window has been opened, so that we can pretend to fail to close it.
1476 lifecycle.deferreds.opening.resolve( {} );
1477 lifecycle.deferreds.opened.resolve( {} );
1478 }
1479
1480 // Turn lifecycle into a Thenable for backwards-compatibility with
1481 // the deprecated nested-promise behaviour, see T163510.
1482 [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
1483 .forEach( function ( method ) {
1484 lifecycle[ method ] = function () {
1485 OO.ui.warnDeprecation(
1486 'Using the return value of closeWindow as a promise is deprecated. ' +
1487 'Use .closeWindow( ... ).closed.' + method + '( ... ) instead.'
1488 );
1489 return compatClosing[ method ].apply( this, arguments );
1490 };
1491 } );
1492
1493 if ( error ) {
1494 compatClosing.reject( new OO.ui.Error( error ) );
1495 lifecycle.deferreds.closing.reject( new OO.ui.Error( error ) );
1496 return lifecycle;
1497 }
1498
1499 // If the window is currently opening, close it when it's done
1500 this.preparingToClose = $.when( this.lifecycle.opened );
1501 // Ensure handlers get called after preparingToClose is set
1502 this.preparingToClose.always( function () {
1503 manager.preparingToClose = null;
1504 manager.emit( 'closing', win, compatClosing, data );
1505 lifecycle.deferreds.closing.resolve( data );
1506 compatOpened = manager.compatOpened;
1507 manager.compatOpened = null;
1508 compatOpened.resolve( compatClosing.promise(), data );
1509 manager.togglePreventIosScrolling( false );
1510 setTimeout( function () {
1511 win.hold( data ).then( function () {
1512 compatClosing.notify( { state: 'hold' } );
1513 setTimeout( function () {
1514 win.teardown( data ).then( function () {
1515 compatClosing.notify( { state: 'teardown' } );
1516 if ( manager.modal ) {
1517 manager.toggleGlobalEvents( false );
1518 manager.toggleAriaIsolation( false );
1519 }
1520 if ( manager.$returnFocusTo && manager.$returnFocusTo.length ) {
1521 manager.$returnFocusTo[ 0 ].focus();
1522 }
1523 manager.currentWindow = null;
1524 manager.lifecycle = null;
1525 lifecycle.deferreds.closed.resolve( data );
1526 compatClosing.resolve( data );
1527 } );
1528 }, manager.getTeardownDelay() );
1529 } );
1530 }, manager.getHoldDelay() );
1531 } );
1532
1533 return lifecycle;
1534 };
1535
1536 /**
1537 * Add windows to the window manager.
1538 *
1539 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
1540 * See the [OOUI documentation on MediaWiki] [2] for examples.
1541 * [2]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
1542 *
1543 * This function can be called in two manners:
1544 *
1545 * 1. `.addWindows( [ winA, winB, ... ] )` (where `winA`, `winB` are OO.ui.Window objects)
1546 *
1547 * This syntax registers windows under the symbolic names defined in their `.static.name`
1548 * properties. For example, if `windowA.constructor.static.name` is `'nameA'`, calling
1549 * `.openWindow( 'nameA' )` afterwards will open the window `windowA`. This syntax requires the
1550 * static name to be set, otherwise an exception will be thrown.
1551 *
1552 * This is the recommended way, as it allows for an easier switch to using a window factory.
1553 *
1554 * 2. `.addWindows( { nameA: winA, nameB: winB, ... } )`
1555 *
1556 * This syntax registers windows under the explicitly given symbolic names. In this example,
1557 * calling `.openWindow( 'nameA' )` afterwards will open the window `windowA`, regardless of what
1558 * its `.static.name` is set to. The static name is not required to be set.
1559 *
1560 * This should only be used if you need to override the default symbolic names.
1561 *
1562 * Example:
1563 *
1564 * var windowManager = new OO.ui.WindowManager();
1565 * $( document.body ).append( windowManager.$element );
1566 *
1567 * // Add a window under the default name: see OO.ui.MessageDialog.static.name
1568 * windowManager.addWindows( [ new OO.ui.MessageDialog() ] );
1569 * // Add a window under an explicit name
1570 * windowManager.addWindows( { myMessageDialog: new OO.ui.MessageDialog() } );
1571 *
1572 * // Open window by default name
1573 * windowManager.openWindow( 'message' );
1574 * // Open window by explicitly given name
1575 * windowManager.openWindow( 'myMessageDialog' );
1576 *
1577 *
1578 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
1579 * by reference, symbolic name, or explicitly defined symbolic names.
1580 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
1581 * explicit nor a statically configured symbolic name.
1582 */
1583 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
1584 var i, len, win, name, list;
1585
1586 if ( Array.isArray( windows ) ) {
1587 // Convert to map of windows by looking up symbolic names from static configuration
1588 list = {};
1589 for ( i = 0, len = windows.length; i < len; i++ ) {
1590 name = windows[ i ].constructor.static.name;
1591 if ( !name ) {
1592 throw new Error( 'Windows must have a `name` static property defined.' );
1593 }
1594 list[ name ] = windows[ i ];
1595 }
1596 } else if ( OO.isPlainObject( windows ) ) {
1597 list = windows;
1598 }
1599
1600 // Add windows
1601 for ( name in list ) {
1602 win = list[ name ];
1603 this.windows[ name ] = win.toggle( false );
1604 this.$element.append( win.$element );
1605 win.setManager( this );
1606 }
1607 };
1608
1609 /**
1610 * Remove the specified windows from the windows manager.
1611 *
1612 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish
1613 * to use the #clearWindows method instead. If you no longer need the window manager and want to
1614 * ensure that it no longer listens to events, use the #destroy method.
1615 *
1616 * @param {string[]} names Symbolic names of windows to remove
1617 * @return {jQuery.Promise} Promise resolved when window is closed and removed
1618 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
1619 */
1620 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
1621 var i, len, win, name, cleanupWindow,
1622 manager = this,
1623 promises = [],
1624 cleanup = function ( name, win ) {
1625 delete manager.windows[ name ];
1626 win.$element.detach();
1627 };
1628
1629 for ( i = 0, len = names.length; i < len; i++ ) {
1630 name = names[ i ];
1631 win = this.windows[ name ];
1632 if ( !win ) {
1633 throw new Error( 'Cannot remove window' );
1634 }
1635 cleanupWindow = cleanup.bind( null, name, win );
1636 promises.push( this.closeWindow( name ).closed.then( cleanupWindow, cleanupWindow ) );
1637 }
1638
1639 return $.when.apply( $, promises );
1640 };
1641
1642 /**
1643 * Remove all windows from the window manager.
1644 *
1645 * Windows will be closed before they are removed. Note that the window manager, though not in use,
1646 * will still listen to events. If the window manager will not be used again, you may wish to use
1647 * the #destroy method instead. To remove just a subset of windows, use the #removeWindows method.
1648 *
1649 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
1650 */
1651 OO.ui.WindowManager.prototype.clearWindows = function () {
1652 return this.removeWindows( Object.keys( this.windows ) );
1653 };
1654
1655 /**
1656 * Set dialog size. In general, this method should not be called directly.
1657 *
1658 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
1659 *
1660 * @param {OO.ui.Window} win Window to update, should be the current window
1661 * @chainable
1662 * @return {OO.ui.WindowManager} The manager, for chaining
1663 */
1664 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
1665 var isFullscreen;
1666
1667 // Bypass for non-current, and thus invisible, windows
1668 if ( win !== this.currentWindow ) {
1669 return;
1670 }
1671
1672 isFullscreen = win.getSize() === 'full';
1673
1674 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
1675 this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
1676 win.setDimensions( win.getSizeProperties() );
1677
1678 this.emit( 'resize', win );
1679
1680 return this;
1681 };
1682
1683 /**
1684 * Prevent scrolling of the document on iOS devices that don't respect `body { overflow: hidden; }`.
1685 *
1686 * This function is called when the window is opened (ready), and so the background is covered up,
1687 * and the user won't see that we're doing weird things to the scroll position.
1688 *
1689 * @private
1690 * @param {boolean} on
1691 * @chainable
1692 * @return {OO.ui.WindowManager} The manager, for chaining
1693 */
1694 OO.ui.WindowManager.prototype.togglePreventIosScrolling = function ( on ) {
1695 var
1696 isIos = /ipad|iphone|ipod/i.test( navigator.userAgent ),
1697 $body = $( this.getElementDocument().body ),
1698 scrollableRoot = OO.ui.Element.static.getRootScrollableElement( $body[ 0 ] ),
1699 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0;
1700
1701 // Only if this is the first/last WindowManager (see #toggleGlobalEvents)
1702 if ( !isIos || stackDepth !== 1 ) {
1703 return this;
1704 }
1705
1706 if ( on ) {
1707 // We can't apply this workaround for non-fullscreen dialogs, because the user would see the
1708 // scroll position change. If they have content that needs scrolling, you're out of luck…
1709 // Always remember the scroll position in case dialog is closed with different size.
1710 this.iosOrigScrollPosition = scrollableRoot.scrollTop;
1711 if ( this.getCurrentWindow().getSize() === 'full' ) {
1712 $body.add( $body.parent() ).addClass( 'oo-ui-windowManager-ios-modal-ready' );
1713 }
1714 } else {
1715 // Always restore ability to scroll in case dialog was opened with different size.
1716 $body.add( $body.parent() ).removeClass( 'oo-ui-windowManager-ios-modal-ready' );
1717 if ( this.getCurrentWindow().getSize() === 'full' ) {
1718 scrollableRoot.scrollTop = this.iosOrigScrollPosition;
1719 }
1720 }
1721 return this;
1722 };
1723
1724 /**
1725 * Bind or unbind global events for scrolling.
1726 *
1727 * @private
1728 * @param {boolean} [on] Bind global events
1729 * @chainable
1730 * @return {OO.ui.WindowManager} The manager, for chaining
1731 */
1732 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
1733 var scrollWidth, bodyMargin,
1734 $body = $( this.getElementDocument().body ),
1735 // We could have multiple window managers open so only modify
1736 // the body css at the bottom of the stack
1737 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0;
1738
1739 on = on === undefined ? !!this.globalEvents : !!on;
1740
1741 if ( on ) {
1742 if ( !this.globalEvents ) {
1743 $( this.getElementWindow() ).on( {
1744 // Start listening for top-level window dimension changes
1745 'orientationchange resize': this.onWindowResizeHandler
1746 } );
1747 if ( stackDepth === 0 ) {
1748 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
1749 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
1750 $body.addClass( 'oo-ui-windowManager-modal-active' );
1751 $body.css( 'margin-right', bodyMargin + scrollWidth );
1752 }
1753 stackDepth++;
1754 this.globalEvents = true;
1755 }
1756 } else if ( this.globalEvents ) {
1757 $( this.getElementWindow() ).off( {
1758 // Stop listening for top-level window dimension changes
1759 'orientationchange resize': this.onWindowResizeHandler
1760 } );
1761 stackDepth--;
1762 if ( stackDepth === 0 ) {
1763 $body.removeClass( 'oo-ui-windowManager-modal-active' );
1764 $body.css( 'margin-right', '' );
1765 }
1766 this.globalEvents = false;
1767 }
1768 $body.data( 'windowManagerGlobalEvents', stackDepth );
1769
1770 return this;
1771 };
1772
1773 /**
1774 * Toggle screen reader visibility of content other than the window manager.
1775 *
1776 * @private
1777 * @param {boolean} [isolate] Make only the window manager visible to screen readers
1778 * @chainable
1779 * @return {OO.ui.WindowManager} The manager, for chaining
1780 */
1781 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
1782 var $topLevelElement;
1783 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
1784
1785 if ( isolate ) {
1786 if ( !this.$ariaHidden ) {
1787 // Find the top level element containing the window manager or the
1788 // window manager's element itself in case its a direct child of body
1789 $topLevelElement = this.$element.parentsUntil( 'body' ).last();
1790 $topLevelElement = $topLevelElement.length === 0 ? this.$element : $topLevelElement;
1791
1792 // In case previously set by another window manager
1793 this.$element.removeAttr( 'aria-hidden' );
1794
1795 // Hide everything other than the window manager from screen readers
1796 this.$ariaHidden = $( document.body )
1797 .children()
1798 .not( 'script' )
1799 .not( $topLevelElement )
1800 .attr( 'aria-hidden', true );
1801 }
1802 } else if ( this.$ariaHidden ) {
1803 // Restore screen reader visibility
1804 this.$ariaHidden.removeAttr( 'aria-hidden' );
1805 this.$ariaHidden = null;
1806
1807 // and hide the window manager
1808 this.$element.attr( 'aria-hidden', true );
1809 }
1810
1811 return this;
1812 };
1813
1814 /**
1815 * Destroy the window manager.
1816 *
1817 * Destroying the window manager ensures that it will no longer listen to events. If you would like
1818 * to continue using the window manager, but wish to remove all windows from it, use the
1819 * #clearWindows method instead.
1820 */
1821 OO.ui.WindowManager.prototype.destroy = function () {
1822 this.toggleGlobalEvents( false );
1823 this.toggleAriaIsolation( false );
1824 this.clearWindows();
1825 this.$element.remove();
1826 };
1827
1828 /**
1829 * A window is a container for elements that are in a child frame. They are used with
1830 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1831 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’,
1832 * ‘medium’, ‘large’), which is interpreted by the window manager. If the requested size is not
1833 * recognized, the window manager will choose a sensible fallback.
1834 *
1835 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1836 * different processes are executed:
1837 *
1838 * **opening**: The opening stage begins when the window manager's
1839 * {@link OO.ui.WindowManager#openWindow openWindow} or the window's {@link #open open} methods are
1840 * used, and the window manager begins to open the window.
1841 *
1842 * - {@link #getSetupProcess} method is called and its result executed
1843 * - {@link #getReadyProcess} method is called and its result executed
1844 *
1845 * **opened**: The window is now open
1846 *
1847 * **closing**: The closing stage begins when the window manager's
1848 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1849 * or the window's {@link #close} methods are used, and the window manager begins to close the
1850 * window.
1851 *
1852 * - {@link #getHoldProcess} method is called and its result executed
1853 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1854 *
1855 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1856 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and
1857 * #getTeardownProcess methods. Note that each {@link OO.ui.Process process} is executed in series,
1858 * so asynchronous processing can complete. Always assume window processes are executed
1859 * asynchronously.
1860 *
1861 * For more information, please see the [OOUI documentation on MediaWiki] [1].
1862 *
1863 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows
1864 *
1865 * @abstract
1866 * @class
1867 * @extends OO.ui.Element
1868 * @mixins OO.EventEmitter
1869 *
1870 * @constructor
1871 * @param {Object} [config] Configuration options
1872 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1873 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
1874 */
1875 OO.ui.Window = function OoUiWindow( config ) {
1876 // Configuration initialization
1877 config = config || {};
1878
1879 // Parent constructor
1880 OO.ui.Window.parent.call( this, config );
1881
1882 // Mixin constructors
1883 OO.EventEmitter.call( this );
1884
1885 // Properties
1886 this.manager = null;
1887 this.size = config.size || this.constructor.static.size;
1888 this.$frame = $( '<div>' );
1889 /**
1890 * Overlay element to use for the `$overlay` configuration option of widgets that support it.
1891 * Things put inside it are overlaid on top of the window and are not bound to its dimensions.
1892 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
1893 *
1894 * MyDialog.prototype.initialize = function () {
1895 * ...
1896 * var popupButton = new OO.ui.PopupButtonWidget( {
1897 * $overlay: this.$overlay,
1898 * label: 'Popup button',
1899 * popup: {
1900 * $content: $( '<p>Popup content.</p><p>More content.</p><p>Yet more content.</p>' ),
1901 * padded: true
1902 * }
1903 * } );
1904 * ...
1905 * };
1906 *
1907 * @property {jQuery}
1908 */
1909 this.$overlay = $( '<div>' );
1910 this.$content = $( '<div>' );
1911
1912 this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
1913 this.$focusTrapAfter = $( '<div>' ).prop( 'tabIndex', 0 );
1914 this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
1915
1916 // Initialization
1917 this.$overlay.addClass( 'oo-ui-window-overlay' );
1918 this.$content
1919 .addClass( 'oo-ui-window-content' )
1920 .attr( 'tabindex', 0 );
1921 this.$frame
1922 .addClass( 'oo-ui-window-frame' )
1923 .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
1924 this.$element
1925 .addClass( 'oo-ui-window' )
1926 .append( this.$frame, this.$overlay );
1927
1928 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1929 // that reference properties not initialized at that time of parent class construction
1930 // TODO: Find a better way to handle post-constructor setup
1931 this.visible = false;
1932 this.$element.addClass( 'oo-ui-element-hidden' );
1933 };
1934
1935 /* Setup */
1936
1937 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1938 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1939
1940 /* Static Properties */
1941
1942 /**
1943 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1944 *
1945 * The static size is used if no #size is configured during construction.
1946 *
1947 * @static
1948 * @inheritable
1949 * @property {string}
1950 */
1951 OO.ui.Window.static.size = 'medium';
1952
1953 /* Methods */
1954
1955 /**
1956 * Handle mouse down events.
1957 *
1958 * @private
1959 * @param {jQuery.Event} e Mouse down event
1960 * @return {OO.ui.Window} The window, for chaining
1961 */
1962 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1963 // Prevent clicking on the click-block from stealing focus
1964 if ( e.target === this.$element[ 0 ] ) {
1965 return false;
1966 }
1967 };
1968
1969 /**
1970 * Check if the window has been initialized.
1971 *
1972 * Initialization occurs when a window is added to a manager.
1973 *
1974 * @return {boolean} Window has been initialized
1975 */
1976 OO.ui.Window.prototype.isInitialized = function () {
1977 return !!this.manager;
1978 };
1979
1980 /**
1981 * Check if the window is visible.
1982 *
1983 * @return {boolean} Window is visible
1984 */
1985 OO.ui.Window.prototype.isVisible = function () {
1986 return this.visible;
1987 };
1988
1989 /**
1990 * Check if the window is opening.
1991 *
1992 * This method is a wrapper around the window manager's
1993 * {@link OO.ui.WindowManager#isOpening isOpening} method.
1994 *
1995 * @return {boolean} Window is opening
1996 */
1997 OO.ui.Window.prototype.isOpening = function () {
1998 return this.manager.isOpening( this );
1999 };
2000
2001 /**
2002 * Check if the window is closing.
2003 *
2004 * This method is a wrapper around the window manager's
2005 * {@link OO.ui.WindowManager#isClosing isClosing} method.
2006 *
2007 * @return {boolean} Window is closing
2008 */
2009 OO.ui.Window.prototype.isClosing = function () {
2010 return this.manager.isClosing( this );
2011 };
2012
2013 /**
2014 * Check if the window is opened.
2015 *
2016 * This method is a wrapper around the window manager's
2017 * {@link OO.ui.WindowManager#isOpened isOpened} method.
2018 *
2019 * @return {boolean} Window is opened
2020 */
2021 OO.ui.Window.prototype.isOpened = function () {
2022 return this.manager.isOpened( this );
2023 };
2024
2025 /**
2026 * Get the window manager.
2027 *
2028 * All windows must be attached to a window manager, which is used to open
2029 * and close the window and control its presentation.
2030 *
2031 * @return {OO.ui.WindowManager} Manager of window
2032 */
2033 OO.ui.Window.prototype.getManager = function () {
2034 return this.manager;
2035 };
2036
2037 /**
2038 * Get the symbolic name of the window size (e.g., `small` or `medium`).
2039 *
2040 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
2041 */
2042 OO.ui.Window.prototype.getSize = function () {
2043 var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ),
2044 sizes = this.manager.constructor.static.sizes,
2045 size = this.size;
2046
2047 if ( !sizes[ size ] ) {
2048 size = this.manager.constructor.static.defaultSize;
2049 }
2050 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
2051 size = 'full';
2052 }
2053
2054 return size;
2055 };
2056
2057 /**
2058 * Get the size properties associated with the current window size
2059 *
2060 * @return {Object} Size properties
2061 */
2062 OO.ui.Window.prototype.getSizeProperties = function () {
2063 return this.manager.constructor.static.sizes[ this.getSize() ];
2064 };
2065
2066 /**
2067 * Disable transitions on window's frame for the duration of the callback function, then enable them
2068 * back.
2069 *
2070 * @private
2071 * @param {Function} callback Function to call while transitions are disabled
2072 */
2073 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
2074 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2075 // Disable transitions first, otherwise we'll get values from when the window was animating.
2076 // We need to build the transition CSS properties using these specific properties since
2077 // Firefox doesn't return anything useful when asked just for 'transition'.
2078 var oldTransition = this.$frame.css( 'transition-property' ) + ' ' +
2079 this.$frame.css( 'transition-duration' ) + ' ' +
2080 this.$frame.css( 'transition-timing-function' ) + ' ' +
2081 this.$frame.css( 'transition-delay' );
2082
2083 this.$frame.css( 'transition', 'none' );
2084 callback();
2085
2086 // Force reflow to make sure the style changes done inside callback
2087 // really are not transitioned
2088 this.$frame.height();
2089 this.$frame.css( 'transition', oldTransition );
2090 };
2091
2092 /**
2093 * Get the height of the full window contents (i.e., the window head, body and foot together).
2094 *
2095 * What constitutes the head, body, and foot varies depending on the window type.
2096 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
2097 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
2098 * and special actions in the head, and dialog content in the body.
2099 *
2100 * To get just the height of the dialog body, use the #getBodyHeight method.
2101 *
2102 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
2103 */
2104 OO.ui.Window.prototype.getContentHeight = function () {
2105 var bodyHeight,
2106 win = this,
2107 bodyStyleObj = this.$body[ 0 ].style,
2108 frameStyleObj = this.$frame[ 0 ].style;
2109
2110 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2111 // Disable transitions first, otherwise we'll get values from when the window was animating.
2112 this.withoutSizeTransitions( function () {
2113 var oldHeight = frameStyleObj.height,
2114 oldPosition = bodyStyleObj.position;
2115 frameStyleObj.height = '1px';
2116 // Force body to resize to new width
2117 bodyStyleObj.position = 'relative';
2118 bodyHeight = win.getBodyHeight();
2119 frameStyleObj.height = oldHeight;
2120 bodyStyleObj.position = oldPosition;
2121 } );
2122
2123 return (
2124 // Add buffer for border
2125 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
2126 // Use combined heights of children
2127 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
2128 );
2129 };
2130
2131 /**
2132 * Get the height of the window body.
2133 *
2134 * To get the height of the full window contents (the window body, head, and foot together),
2135 * use #getContentHeight.
2136 *
2137 * When this function is called, the window will temporarily have been resized
2138 * to height=1px, so .scrollHeight measurements can be taken accurately.
2139 *
2140 * @return {number} Height of the window body in pixels
2141 */
2142 OO.ui.Window.prototype.getBodyHeight = function () {
2143 return this.$body[ 0 ].scrollHeight;
2144 };
2145
2146 /**
2147 * Get the directionality of the frame (right-to-left or left-to-right).
2148 *
2149 * @return {string} Directionality: `'ltr'` or `'rtl'`
2150 */
2151 OO.ui.Window.prototype.getDir = function () {
2152 return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2153 };
2154
2155 /**
2156 * Get the 'setup' process.
2157 *
2158 * The setup process is used to set up a window for use in a particular context, based on the `data`
2159 * argument. This method is called during the opening phase of the window’s lifecycle (before the
2160 * opening animation). You can add elements to the window in this process or set their default
2161 * values.
2162 *
2163 * Override this method to add additional steps to the ‘setup’ process the parent method provides
2164 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2165 * of OO.ui.Process.
2166 *
2167 * To add window content that persists between openings, you may wish to use the #initialize method
2168 * instead.
2169 *
2170 * @param {Object} [data] Window opening data
2171 * @return {OO.ui.Process} Setup process
2172 */
2173 OO.ui.Window.prototype.getSetupProcess = function () {
2174 return new OO.ui.Process();
2175 };
2176
2177 /**
2178 * Get the ‘ready’ process.
2179 *
2180 * The ready process is used to ready a window for use in a particular context, based on the `data`
2181 * argument. This method is called during the opening phase of the window’s lifecycle, after the
2182 * window has been {@link #getSetupProcess setup} (after the opening animation). You can focus
2183 * elements in the window in this process, or open their dropdowns.
2184 *
2185 * Override this method to add additional steps to the ‘ready’ process the parent method
2186 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2187 * methods of OO.ui.Process.
2188 *
2189 * @param {Object} [data] Window opening data
2190 * @return {OO.ui.Process} Ready process
2191 */
2192 OO.ui.Window.prototype.getReadyProcess = function () {
2193 return new OO.ui.Process();
2194 };
2195
2196 /**
2197 * Get the 'hold' process.
2198 *
2199 * The hold process is used to keep a window from being used in a particular context, based on the
2200 * `data` argument. This method is called during the closing phase of the window’s lifecycle (before
2201 * the closing animation). You can close dropdowns of elements in the window in this process, if
2202 * they do not get closed automatically.
2203 *
2204 * Override this method to add additional steps to the 'hold' process the parent method provides
2205 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2206 * of OO.ui.Process.
2207 *
2208 * @param {Object} [data] Window closing data
2209 * @return {OO.ui.Process} Hold process
2210 */
2211 OO.ui.Window.prototype.getHoldProcess = function () {
2212 return new OO.ui.Process();
2213 };
2214
2215 /**
2216 * Get the ‘teardown’ process.
2217 *
2218 * The teardown process is used to teardown a window after use. During teardown, user interactions
2219 * within the window are conveyed and the window is closed, based on the `data` argument. This
2220 * method is called during the closing phase of the window’s lifecycle (after the closing
2221 * animation). You can remove elements in the window in this process or clear their values.
2222 *
2223 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2224 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2225 * of OO.ui.Process.
2226 *
2227 * @param {Object} [data] Window closing data
2228 * @return {OO.ui.Process} Teardown process
2229 */
2230 OO.ui.Window.prototype.getTeardownProcess = function () {
2231 return new OO.ui.Process();
2232 };
2233
2234 /**
2235 * Set the window manager.
2236 *
2237 * This will cause the window to initialize. Calling it more than once will cause an error.
2238 *
2239 * @param {OO.ui.WindowManager} manager Manager for this window
2240 * @throws {Error} An error is thrown if the method is called more than once
2241 * @chainable
2242 * @return {OO.ui.Window} The window, for chaining
2243 */
2244 OO.ui.Window.prototype.setManager = function ( manager ) {
2245 if ( this.manager ) {
2246 throw new Error( 'Cannot set window manager, window already has a manager' );
2247 }
2248
2249 this.manager = manager;
2250 this.initialize();
2251
2252 return this;
2253 };
2254
2255 /**
2256 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2257 *
2258 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2259 * `full`
2260 * @chainable
2261 * @return {OO.ui.Window} The window, for chaining
2262 */
2263 OO.ui.Window.prototype.setSize = function ( size ) {
2264 this.size = size;
2265 this.updateSize();
2266 return this;
2267 };
2268
2269 /**
2270 * Update the window size.
2271 *
2272 * @throws {Error} An error is thrown if the window is not attached to a window manager
2273 * @chainable
2274 * @return {OO.ui.Window} The window, for chaining
2275 */
2276 OO.ui.Window.prototype.updateSize = function () {
2277 if ( !this.manager ) {
2278 throw new Error( 'Cannot update window size, must be attached to a manager' );
2279 }
2280
2281 this.manager.updateWindowSize( this );
2282
2283 return this;
2284 };
2285
2286 /**
2287 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2288 * when the window is opening. In general, setDimensions should not be called directly.
2289 *
2290 * To set the size of the window, use the #setSize method.
2291 *
2292 * @param {Object} dim CSS dimension properties
2293 * @param {string|number} [dim.width] Width
2294 * @param {string|number} [dim.minWidth] Minimum width
2295 * @param {string|number} [dim.maxWidth] Maximum width
2296 * @param {string|number} [dim.height] Height, omit to set based on height of contents
2297 * @param {string|number} [dim.minHeight] Minimum height
2298 * @param {string|number} [dim.maxHeight] Maximum height
2299 * @chainable
2300 * @return {OO.ui.Window} The window, for chaining
2301 */
2302 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2303 var height,
2304 win = this,
2305 styleObj = this.$frame[ 0 ].style;
2306
2307 // Calculate the height we need to set using the correct width
2308 if ( dim.height === undefined ) {
2309 this.withoutSizeTransitions( function () {
2310 var oldWidth = styleObj.width;
2311 win.$frame.css( 'width', dim.width || '' );
2312 height = win.getContentHeight();
2313 styleObj.width = oldWidth;
2314 } );
2315 } else {
2316 height = dim.height;
2317 }
2318
2319 this.$frame.css( {
2320 width: dim.width || '',
2321 minWidth: dim.minWidth || '',
2322 maxWidth: dim.maxWidth || '',
2323 height: height || '',
2324 minHeight: dim.minHeight || '',
2325 maxHeight: dim.maxHeight || ''
2326 } );
2327
2328 return this;
2329 };
2330
2331 /**
2332 * Initialize window contents.
2333 *
2334 * Before the window is opened for the first time, #initialize is called so that content that
2335 * persists between openings can be added to the window.
2336 *
2337 * To set up a window with new content each time the window opens, use #getSetupProcess.
2338 *
2339 * @throws {Error} An error is thrown if the window is not attached to a window manager
2340 * @chainable
2341 * @return {OO.ui.Window} The window, for chaining
2342 */
2343 OO.ui.Window.prototype.initialize = function () {
2344 if ( !this.manager ) {
2345 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2346 }
2347
2348 // Properties
2349 this.$head = $( '<div>' );
2350 this.$body = $( '<div>' );
2351 this.$foot = $( '<div>' );
2352 this.$document = $( this.getElementDocument() );
2353
2354 // Events
2355 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2356
2357 // Initialization
2358 this.$head.addClass( 'oo-ui-window-head' );
2359 this.$body.addClass( 'oo-ui-window-body' );
2360 this.$foot.addClass( 'oo-ui-window-foot' );
2361 this.$content.append( this.$head, this.$body, this.$foot );
2362
2363 return this;
2364 };
2365
2366 /**
2367 * Called when someone tries to focus the hidden element at the end of the dialog.
2368 * Sends focus back to the start of the dialog.
2369 *
2370 * @param {jQuery.Event} event Focus event
2371 */
2372 OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
2373 var backwards = this.$focusTrapBefore.is( event.target ),
2374 element = OO.ui.findFocusable( this.$content, backwards );
2375 if ( element ) {
2376 // There's a focusable element inside the content, at the front or
2377 // back depending on which focus trap we hit; select it.
2378 element.focus();
2379 } else {
2380 // There's nothing focusable inside the content. As a fallback,
2381 // this.$content is focusable, and focusing it will keep our focus
2382 // properly trapped. It's not a *meaningful* focus, since it's just
2383 // the content-div for the Window, but it's better than letting focus
2384 // escape into the page.
2385 this.$content.trigger( 'focus' );
2386 }
2387 };
2388
2389 /**
2390 * Open the window.
2391 *
2392 * This method is a wrapper around a call to the window
2393 * manager’s {@link OO.ui.WindowManager#openWindow openWindow} method.
2394 *
2395 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2396 *
2397 * @param {Object} [data] Window opening data
2398 * @return {OO.ui.WindowInstance} See OO.ui.WindowManager#openWindow
2399 * @throws {Error} An error is thrown if the window is not attached to a window manager
2400 */
2401 OO.ui.Window.prototype.open = function ( data ) {
2402 if ( !this.manager ) {
2403 throw new Error( 'Cannot open window, must be attached to a manager' );
2404 }
2405
2406 return this.manager.openWindow( this, data );
2407 };
2408
2409 /**
2410 * Close the window.
2411 *
2412 * This method is a wrapper around a call to the window
2413 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method.
2414 *
2415 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2416 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2417 * the window closes.
2418 *
2419 * @param {Object} [data] Window closing data
2420 * @return {OO.ui.WindowInstance} See OO.ui.WindowManager#closeWindow
2421 * @throws {Error} An error is thrown if the window is not attached to a window manager
2422 */
2423 OO.ui.Window.prototype.close = function ( data ) {
2424 if ( !this.manager ) {
2425 throw new Error( 'Cannot close window, must be attached to a manager' );
2426 }
2427
2428 return this.manager.closeWindow( this, data );
2429 };
2430
2431 /**
2432 * Setup window.
2433 *
2434 * This is called by OO.ui.WindowManager during window opening (before the animation), and should
2435 * not be called directly by other systems.
2436 *
2437 * @param {Object} [data] Window opening data
2438 * @return {jQuery.Promise} Promise resolved when window is setup
2439 */
2440 OO.ui.Window.prototype.setup = function ( data ) {
2441 var win = this;
2442
2443 this.toggle( true );
2444
2445 this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
2446 this.$focusTraps.on( 'focus', this.focusTrapHandler );
2447
2448 return this.getSetupProcess( data ).execute().then( function () {
2449 win.updateSize();
2450 // Force redraw by asking the browser to measure the elements' widths
2451 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2452 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2453 } );
2454 };
2455
2456 /**
2457 * Ready window.
2458 *
2459 * This is called by OO.ui.WindowManager during window opening (after the animation), and should not
2460 * be called directly by other systems.
2461 *
2462 * @param {Object} [data] Window opening data
2463 * @return {jQuery.Promise} Promise resolved when window is ready
2464 */
2465 OO.ui.Window.prototype.ready = function ( data ) {
2466 var win = this;
2467
2468 this.$content.trigger( 'focus' );
2469 return this.getReadyProcess( data ).execute().then( function () {
2470 // Force redraw by asking the browser to measure the elements' widths
2471 win.$element.addClass( 'oo-ui-window-ready' ).width();
2472 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2473 } );
2474 };
2475
2476 /**
2477 * Hold window.
2478 *
2479 * This is called by OO.ui.WindowManager during window closing (before the animation), and should
2480 * not be called directly by other systems.
2481 *
2482 * @param {Object} [data] Window closing data
2483 * @return {jQuery.Promise} Promise resolved when window is held
2484 */
2485 OO.ui.Window.prototype.hold = function ( data ) {
2486 var win = this;
2487
2488 return this.getHoldProcess( data ).execute().then( function () {
2489 // Get the focused element within the window's content
2490 var $focus = win.$content.find(
2491 OO.ui.Element.static.getDocument( win.$content ).activeElement
2492 );
2493
2494 // Blur the focused element
2495 if ( $focus.length ) {
2496 $focus[ 0 ].blur();
2497 }
2498
2499 // Force redraw by asking the browser to measure the elements' widths
2500 win.$element.removeClass( 'oo-ui-window-ready oo-ui-window-setup' ).width();
2501 win.$content.removeClass( 'oo-ui-window-content-ready oo-ui-window-content-setup' ).width();
2502 } );
2503 };
2504
2505 /**
2506 * Teardown window.
2507 *
2508 * This is called by OO.ui.WindowManager during window closing (after the animation), and should not
2509 * be called directly by other systems.
2510 *
2511 * @param {Object} [data] Window closing data
2512 * @return {jQuery.Promise} Promise resolved when window is torn down
2513 */
2514 OO.ui.Window.prototype.teardown = function ( data ) {
2515 var win = this;
2516
2517 return this.getTeardownProcess( data ).execute().then( function () {
2518 // Force redraw by asking the browser to measure the elements' widths
2519 win.$element.removeClass( 'oo-ui-window-active' ).width();
2520
2521 win.$focusTraps.off( 'focus', win.focusTrapHandler );
2522 win.toggle( false );
2523 } );
2524 };
2525
2526 /**
2527 * The Dialog class serves as the base class for the other types of dialogs.
2528 * Unless extended to include controls, the rendered dialog box is a simple window
2529 * that users can close by hitting the Escape key. Dialog windows are used with OO.ui.WindowManager,
2530 * which opens, closes, and controls the presentation of the window. See the
2531 * [OOUI documentation on MediaWiki] [1] for more information.
2532 *
2533 * @example
2534 * // A simple dialog window.
2535 * function MyDialog( config ) {
2536 * MyDialog.parent.call( this, config );
2537 * }
2538 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2539 * MyDialog.static.name = 'myDialog';
2540 * MyDialog.prototype.initialize = function () {
2541 * MyDialog.parent.prototype.initialize.call( this );
2542 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2543 * this.content.$element.append( '<p>A simple dialog window. Press Escape key to ' +
2544 * 'close.</p>' );
2545 * this.$body.append( this.content.$element );
2546 * };
2547 * MyDialog.prototype.getBodyHeight = function () {
2548 * return this.content.$element.outerHeight( true );
2549 * };
2550 * var myDialog = new MyDialog( {
2551 * size: 'medium'
2552 * } );
2553 * // Create and append a window manager, which opens and closes the window.
2554 * var windowManager = new OO.ui.WindowManager();
2555 * $( document.body ).append( windowManager.$element );
2556 * windowManager.addWindows( [ myDialog ] );
2557 * // Open the window!
2558 * windowManager.openWindow( myDialog );
2559 *
2560 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Dialogs
2561 *
2562 * @abstract
2563 * @class
2564 * @extends OO.ui.Window
2565 * @mixins OO.ui.mixin.PendingElement
2566 *
2567 * @constructor
2568 * @param {Object} [config] Configuration options
2569 */
2570 OO.ui.Dialog = function OoUiDialog( config ) {
2571 // Parent constructor
2572 OO.ui.Dialog.parent.call( this, config );
2573
2574 // Mixin constructors
2575 OO.ui.mixin.PendingElement.call( this );
2576
2577 // Properties
2578 this.actions = new OO.ui.ActionSet();
2579 this.attachedActions = [];
2580 this.currentAction = null;
2581 this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
2582
2583 // Events
2584 this.actions.connect( this, {
2585 click: 'onActionClick',
2586 change: 'onActionsChange'
2587 } );
2588
2589 // Initialization
2590 this.$element
2591 .addClass( 'oo-ui-dialog' )
2592 .attr( 'role', 'dialog' );
2593 };
2594
2595 /* Setup */
2596
2597 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2598 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2599
2600 /* Static Properties */
2601
2602 /**
2603 * Symbolic name of dialog.
2604 *
2605 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2606 * Please see the [OOUI documentation on MediaWiki] [3] for more information.
2607 *
2608 * [3]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
2609 *
2610 * @abstract
2611 * @static
2612 * @inheritable
2613 * @property {string}
2614 */
2615 OO.ui.Dialog.static.name = '';
2616
2617 /**
2618 * The dialog title.
2619 *
2620 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node,
2621 * or a function that will produce a Label node or string. The title can also be specified with data
2622 * passed to the constructor (see #getSetupProcess). In this case, the static value will be
2623 * overridden.
2624 *
2625 * @abstract
2626 * @static
2627 * @inheritable
2628 * @property {jQuery|string|Function}
2629 */
2630 OO.ui.Dialog.static.title = '';
2631
2632 /**
2633 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2634 *
2635 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this
2636 * case, the static value will be overridden.
2637 *
2638 * [2]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
2639 *
2640 * @static
2641 * @inheritable
2642 * @property {Object[]}
2643 */
2644 OO.ui.Dialog.static.actions = [];
2645
2646 /**
2647 * Close the dialog when the Escape key is pressed.
2648 *
2649 * @static
2650 * @abstract
2651 * @inheritable
2652 * @property {boolean}
2653 */
2654 OO.ui.Dialog.static.escapable = true;
2655
2656 /* Methods */
2657
2658 /**
2659 * Handle frame document key down events.
2660 *
2661 * @private
2662 * @param {jQuery.Event} e Key down event
2663 */
2664 OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
2665 var actions;
2666 if ( e.which === OO.ui.Keys.ESCAPE && this.constructor.static.escapable ) {
2667 this.executeAction( '' );
2668 e.preventDefault();
2669 e.stopPropagation();
2670 } else if ( e.which === OO.ui.Keys.ENTER && ( e.ctrlKey || e.metaKey ) ) {
2671 actions = this.actions.get( { flags: 'primary', visible: true, disabled: false } );
2672 if ( actions.length > 0 ) {
2673 this.executeAction( actions[ 0 ].getAction() );
2674 e.preventDefault();
2675 e.stopPropagation();
2676 }
2677 }
2678 };
2679
2680 /**
2681 * Handle action click events.
2682 *
2683 * @private
2684 * @param {OO.ui.ActionWidget} action Action that was clicked
2685 */
2686 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2687 if ( !this.isPending() ) {
2688 this.executeAction( action.getAction() );
2689 }
2690 };
2691
2692 /**
2693 * Handle actions change event.
2694 *
2695 * @private
2696 */
2697 OO.ui.Dialog.prototype.onActionsChange = function () {
2698 this.detachActions();
2699 if ( !this.isClosing() ) {
2700 this.attachActions();
2701 if ( !this.isOpening() ) {
2702 // If the dialog is currently opening, this will be called automatically soon.
2703 this.updateSize();
2704 }
2705 }
2706 };
2707
2708 /**
2709 * Get the set of actions used by the dialog.
2710 *
2711 * @return {OO.ui.ActionSet}
2712 */
2713 OO.ui.Dialog.prototype.getActions = function () {
2714 return this.actions;
2715 };
2716
2717 /**
2718 * Get a process for taking action.
2719 *
2720 * When you override this method, you can create a new OO.ui.Process and return it, or add
2721 * additional accept steps to the process the parent method provides using the
2722 * {@link OO.ui.Process#first 'first'} and {@link OO.ui.Process#next 'next'} methods of
2723 * OO.ui.Process.
2724 *
2725 * @param {string} [action] Symbolic name of action
2726 * @return {OO.ui.Process} Action process
2727 */
2728 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2729 return new OO.ui.Process()
2730 .next( function () {
2731 if ( !action ) {
2732 // An empty action always closes the dialog without data, which should always be
2733 // safe and make no changes
2734 this.close();
2735 }
2736 }, this );
2737 };
2738
2739 /**
2740 * @inheritdoc
2741 *
2742 * @param {Object} [data] Dialog opening data
2743 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2744 * the {@link #static-title static title}
2745 * @param {Object[]} [data.actions] List of configuration options for each
2746 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2747 */
2748 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2749 data = data || {};
2750
2751 // Parent method
2752 return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
2753 .next( function () {
2754 var config = this.constructor.static,
2755 actions = data.actions !== undefined ? data.actions : config.actions,
2756 title = data.title !== undefined ? data.title : config.title;
2757
2758 this.title.setLabel( title ).setTitle( title );
2759 this.actions.add( this.getActionWidgets( actions ) );
2760
2761 this.$element.on( 'keydown', this.onDialogKeyDownHandler );
2762 }, this );
2763 };
2764
2765 /**
2766 * @inheritdoc
2767 */
2768 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2769 // Parent method
2770 return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
2771 .first( function () {
2772 this.$element.off( 'keydown', this.onDialogKeyDownHandler );
2773
2774 this.actions.clear();
2775 this.currentAction = null;
2776 }, this );
2777 };
2778
2779 /**
2780 * @inheritdoc
2781 */
2782 OO.ui.Dialog.prototype.initialize = function () {
2783 // Parent method
2784 OO.ui.Dialog.parent.prototype.initialize.call( this );
2785
2786 // Properties
2787 this.title = new OO.ui.LabelWidget();
2788
2789 // Initialization
2790 this.$content.addClass( 'oo-ui-dialog-content' );
2791 this.$element.attr( 'aria-labelledby', this.title.getElementId() );
2792 this.setPendingElement( this.$head );
2793 };
2794
2795 /**
2796 * Get action widgets from a list of configs
2797 *
2798 * @param {Object[]} actions Action widget configs
2799 * @return {OO.ui.ActionWidget[]} Action widgets
2800 */
2801 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2802 var i, len, widgets = [];
2803 for ( i = 0, len = actions.length; i < len; i++ ) {
2804 widgets.push( this.getActionWidget( actions[ i ] ) );
2805 }
2806 return widgets;
2807 };
2808
2809 /**
2810 * Get action widget from config
2811 *
2812 * Override this method to change the action widget class used.
2813 *
2814 * @param {Object} config Action widget config
2815 * @return {OO.ui.ActionWidget} Action widget
2816 */
2817 OO.ui.Dialog.prototype.getActionWidget = function ( config ) {
2818 return new OO.ui.ActionWidget( this.getActionWidgetConfig( config ) );
2819 };
2820
2821 /**
2822 * Get action widget config
2823 *
2824 * Override this method to modify the action widget config
2825 *
2826 * @param {Object} config Initial action widget config
2827 * @return {Object} Action widget config
2828 */
2829 OO.ui.Dialog.prototype.getActionWidgetConfig = function ( config ) {
2830 return config;
2831 };
2832
2833 /**
2834 * Attach action actions.
2835 *
2836 * @protected
2837 */
2838 OO.ui.Dialog.prototype.attachActions = function () {
2839 // Remember the list of potentially attached actions
2840 this.attachedActions = this.actions.get();
2841 };
2842
2843 /**
2844 * Detach action actions.
2845 *
2846 * @protected
2847 * @chainable
2848 * @return {OO.ui.Dialog} The dialog, for chaining
2849 */
2850 OO.ui.Dialog.prototype.detachActions = function () {
2851 var i, len;
2852
2853 // Detach all actions that may have been previously attached
2854 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2855 this.attachedActions[ i ].$element.detach();
2856 }
2857 this.attachedActions = [];
2858
2859 return this;
2860 };
2861
2862 /**
2863 * Execute an action.
2864 *
2865 * @param {string} action Symbolic name of action to execute
2866 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2867 */
2868 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2869 this.pushPending();
2870 this.currentAction = action;
2871 return this.getActionProcess( action ).execute()
2872 .always( this.popPending.bind( this ) );
2873 };
2874
2875 /**
2876 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
2877 * consists of a header that contains the dialog title, a body with the message, and a footer that
2878 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
2879 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
2880 *
2881 * There are two basic types of message dialogs, confirmation and alert:
2882 *
2883 * - **confirmation**: the dialog title describes what a progressive action will do and the message
2884 * provides more details about the consequences.
2885 * - **alert**: the dialog title describes which event occurred and the message provides more
2886 * information about why the event occurred.
2887 *
2888 * The MessageDialog class specifies two actions: ‘accept’, the primary
2889 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
2890 * passing along the selected action.
2891 *
2892 * For more information and examples, please see the [OOUI documentation on MediaWiki][1].
2893 *
2894 * @example
2895 * // Example: Creating and opening a message dialog window.
2896 * var messageDialog = new OO.ui.MessageDialog();
2897 *
2898 * // Create and append a window manager.
2899 * var windowManager = new OO.ui.WindowManager();
2900 * $( document.body ).append( windowManager.$element );
2901 * windowManager.addWindows( [ messageDialog ] );
2902 * // Open the window.
2903 * windowManager.openWindow( messageDialog, {
2904 * title: 'Basic message dialog',
2905 * message: 'This is the message'
2906 * } );
2907 *
2908 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
2909 *
2910 * @class
2911 * @extends OO.ui.Dialog
2912 *
2913 * @constructor
2914 * @param {Object} [config] Configuration options
2915 */
2916 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
2917 // Parent constructor
2918 OO.ui.MessageDialog.parent.call( this, config );
2919
2920 // Properties
2921 this.verticalActionLayout = null;
2922
2923 // Initialization
2924 this.$element.addClass( 'oo-ui-messageDialog' );
2925 };
2926
2927 /* Setup */
2928
2929 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
2930
2931 /* Static Properties */
2932
2933 /**
2934 * @static
2935 * @inheritdoc
2936 */
2937 OO.ui.MessageDialog.static.name = 'message';
2938
2939 /**
2940 * @static
2941 * @inheritdoc
2942 */
2943 OO.ui.MessageDialog.static.size = 'small';
2944
2945 /**
2946 * Dialog title.
2947 *
2948 * The title of a confirmation dialog describes what a progressive action will do. The
2949 * title of an alert dialog describes which event occurred.
2950 *
2951 * @static
2952 * @inheritable
2953 * @property {jQuery|string|Function|null}
2954 */
2955 OO.ui.MessageDialog.static.title = null;
2956
2957 /**
2958 * The message displayed in the dialog body.
2959 *
2960 * A confirmation message describes the consequences of a progressive action. An alert
2961 * message describes why an event occurred.
2962 *
2963 * @static
2964 * @inheritable
2965 * @property {jQuery|string|Function|null}
2966 */
2967 OO.ui.MessageDialog.static.message = null;
2968
2969 /**
2970 * @static
2971 * @inheritdoc
2972 */
2973 OO.ui.MessageDialog.static.actions = [
2974 // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
2975 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
2976 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
2977 ];
2978
2979 /* Methods */
2980
2981 /**
2982 * Toggle action layout between vertical and horizontal.
2983 *
2984 * @private
2985 * @param {boolean} [value] Layout actions vertically, omit to toggle
2986 * @chainable
2987 * @return {OO.ui.MessageDialog} The dialog, for chaining
2988 */
2989 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
2990 value = value === undefined ? !this.verticalActionLayout : !!value;
2991
2992 if ( value !== this.verticalActionLayout ) {
2993 this.verticalActionLayout = value;
2994 this.$actions
2995 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
2996 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
2997 }
2998
2999 return this;
3000 };
3001
3002 /**
3003 * @inheritdoc
3004 */
3005 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
3006 if ( action ) {
3007 return new OO.ui.Process( function () {
3008 this.close( { action: action } );
3009 }, this );
3010 }
3011 return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
3012 };
3013
3014 /**
3015 * @inheritdoc
3016 *
3017 * @param {Object} [data] Dialog opening data
3018 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
3019 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
3020 * @param {string} [data.size] Symbolic name of the dialog size, see OO.ui.Window
3021 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
3022 * action item
3023 */
3024 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
3025 data = data || {};
3026
3027 // Parent method
3028 return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
3029 .next( function () {
3030 this.title.setLabel(
3031 data.title !== undefined ? data.title : this.constructor.static.title
3032 );
3033 this.message.setLabel(
3034 data.message !== undefined ? data.message : this.constructor.static.message
3035 );
3036 this.size = data.size !== undefined ? data.size : this.constructor.static.size;
3037 }, this );
3038 };
3039
3040 /**
3041 * @inheritdoc
3042 */
3043 OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
3044 data = data || {};
3045
3046 // Parent method
3047 return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data )
3048 .next( function () {
3049 // Focus the primary action button
3050 var actions = this.actions.get();
3051 actions = actions.filter( function ( action ) {
3052 return action.getFlags().indexOf( 'primary' ) > -1;
3053 } );
3054 if ( actions.length > 0 ) {
3055 actions[ 0 ].focus();
3056 }
3057 }, this );
3058 };
3059
3060 /**
3061 * @inheritdoc
3062 */
3063 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
3064 var bodyHeight, oldOverflow,
3065 $scrollable = this.container.$element;
3066
3067 oldOverflow = $scrollable[ 0 ].style.overflow;
3068 $scrollable[ 0 ].style.overflow = 'hidden';
3069
3070 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
3071
3072 bodyHeight = this.text.$element.outerHeight( true );
3073 $scrollable[ 0 ].style.overflow = oldOverflow;
3074
3075 return bodyHeight;
3076 };
3077
3078 /**
3079 * @inheritdoc
3080 */
3081 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
3082 var
3083 dialog = this,
3084 $scrollable = this.container.$element;
3085 OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
3086
3087 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
3088 // Need to do it after transition completes (250ms), add 50ms just in case.
3089 setTimeout( function () {
3090 var oldOverflow = $scrollable[ 0 ].style.overflow,
3091 activeElement = document.activeElement;
3092
3093 $scrollable[ 0 ].style.overflow = 'hidden';
3094
3095 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
3096
3097 // Check reconsiderScrollbars didn't destroy our focus, as we
3098 // are doing this after the ready process.
3099 if ( activeElement && activeElement !== document.activeElement && activeElement.focus ) {
3100 activeElement.focus();
3101 }
3102
3103 $scrollable[ 0 ].style.overflow = oldOverflow;
3104 }, 300 );
3105
3106 dialog.fitActions();
3107 // Wait for CSS transition to finish and do it again :(
3108 setTimeout( function () {
3109 dialog.fitActions();
3110 }, 300 );
3111
3112 return this;
3113 };
3114
3115 /**
3116 * @inheritdoc
3117 */
3118 OO.ui.MessageDialog.prototype.initialize = function () {
3119 // Parent method
3120 OO.ui.MessageDialog.parent.prototype.initialize.call( this );
3121
3122 // Properties
3123 this.$actions = $( '<div>' );
3124 this.container = new OO.ui.PanelLayout( {
3125 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
3126 } );
3127 this.text = new OO.ui.PanelLayout( {
3128 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
3129 } );
3130 this.message = new OO.ui.LabelWidget( {
3131 classes: [ 'oo-ui-messageDialog-message' ]
3132 } );
3133
3134 // Initialization
3135 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
3136 this.$content.addClass( 'oo-ui-messageDialog-content' );
3137 this.container.$element.append( this.text.$element );
3138 this.text.$element.append( this.title.$element, this.message.$element );
3139 this.$body.append( this.container.$element );
3140 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
3141 this.$foot.append( this.$actions );
3142 };
3143
3144 /**
3145 * @inheritdoc
3146 */
3147 OO.ui.MessageDialog.prototype.getActionWidgetConfig = function ( config ) {
3148 // Force unframed
3149 return $.extend( {}, config, { framed: false } );
3150 };
3151
3152 /**
3153 * @inheritdoc
3154 */
3155 OO.ui.MessageDialog.prototype.attachActions = function () {
3156 var i, len, special, others;
3157
3158 // Parent method
3159 OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
3160
3161 special = this.actions.getSpecial();
3162 others = this.actions.getOthers();
3163
3164 if ( special.safe ) {
3165 this.$actions.append( special.safe.$element );
3166 special.safe.toggleFramed( true );
3167 }
3168 for ( i = 0, len = others.length; i < len; i++ ) {
3169 this.$actions.append( others[ i ].$element );
3170 others[ i ].toggleFramed( true );
3171 }
3172 if ( special.primary ) {
3173 this.$actions.append( special.primary.$element );
3174 special.primary.toggleFramed( true );
3175 }
3176 };
3177
3178 /**
3179 * Fit action actions into columns or rows.
3180 *
3181 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
3182 *
3183 * @private
3184 */
3185 OO.ui.MessageDialog.prototype.fitActions = function () {
3186 var i, len, action,
3187 previous = this.verticalActionLayout,
3188 actions = this.actions.get();
3189
3190 // Detect clipping
3191 this.toggleVerticalActionLayout( false );
3192 for ( i = 0, len = actions.length; i < len; i++ ) {
3193 action = actions[ i ];
3194 if ( action.$element[ 0 ].scrollWidth > action.$element[ 0 ].clientWidth ) {
3195 this.toggleVerticalActionLayout( true );
3196 break;
3197 }
3198 }
3199
3200 // Move the body out of the way of the foot
3201 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
3202
3203 if ( this.verticalActionLayout !== previous ) {
3204 // We changed the layout, window height might need to be updated.
3205 this.updateSize();
3206 }
3207 };
3208
3209 /**
3210 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
3211 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
3212 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again
3213 * when relevant. The ProcessDialog class is always extended and customized with the actions and
3214 * content required for each process.
3215 *
3216 * The process dialog box consists of a header that visually represents the ‘working’ state of long
3217 * processes with an animation. The header contains the dialog title as well as
3218 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
3219 * a ‘primary’ action on the right (e.g., ‘Done’).
3220 *
3221 * Like other windows, the process dialog is managed by a
3222 * {@link OO.ui.WindowManager window manager}.
3223 * Please see the [OOUI documentation on MediaWiki][1] for more information and examples.
3224 *
3225 * @example
3226 * // Example: Creating and opening a process dialog window.
3227 * function MyProcessDialog( config ) {
3228 * MyProcessDialog.parent.call( this, config );
3229 * }
3230 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
3231 *
3232 * MyProcessDialog.static.name = 'myProcessDialog';
3233 * MyProcessDialog.static.title = 'Process dialog';
3234 * MyProcessDialog.static.actions = [
3235 * { action: 'save', label: 'Done', flags: 'primary' },
3236 * { label: 'Cancel', flags: 'safe' }
3237 * ];
3238 *
3239 * MyProcessDialog.prototype.initialize = function () {
3240 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
3241 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
3242 * this.content.$element.append( '<p>This is a process dialog window. The header ' +
3243 * 'contains the title and two buttons: \'Cancel\' (a safe action) on the left and ' +
3244 * '\'Done\' (a primary action) on the right.</p>' );
3245 * this.$body.append( this.content.$element );
3246 * };
3247 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
3248 * var dialog = this;
3249 * if ( action ) {
3250 * return new OO.ui.Process( function () {
3251 * dialog.close( { action: action } );
3252 * } );
3253 * }
3254 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
3255 * };
3256 *
3257 * var windowManager = new OO.ui.WindowManager();
3258 * $( document.body ).append( windowManager.$element );
3259 *
3260 * var dialog = new MyProcessDialog();
3261 * windowManager.addWindows( [ dialog ] );
3262 * windowManager.openWindow( dialog );
3263 *
3264 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs
3265 *
3266 * @abstract
3267 * @class
3268 * @extends OO.ui.Dialog
3269 *
3270 * @constructor
3271 * @param {Object} [config] Configuration options
3272 */
3273 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
3274 // Parent constructor
3275 OO.ui.ProcessDialog.parent.call( this, config );
3276
3277 // Properties
3278 this.fitOnOpen = false;
3279
3280 // Initialization
3281 this.$element.addClass( 'oo-ui-processDialog' );
3282 if ( OO.ui.isMobile() ) {
3283 this.$element.addClass( 'oo-ui-isMobile' );
3284 }
3285 };
3286
3287 /* Setup */
3288
3289 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
3290
3291 /* Methods */
3292
3293 /**
3294 * Handle dismiss button click events.
3295 *
3296 * Hides errors.
3297 *
3298 * @private
3299 */
3300 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
3301 this.hideErrors();
3302 };
3303
3304 /**
3305 * Handle retry button click events.
3306 *
3307 * Hides errors and then tries again.
3308 *
3309 * @private
3310 */
3311 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
3312 this.hideErrors();
3313 this.executeAction( this.currentAction );
3314 };
3315
3316 /**
3317 * @inheritdoc
3318 */
3319 OO.ui.ProcessDialog.prototype.initialize = function () {
3320 // Parent method
3321 OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
3322
3323 // Properties
3324 this.$navigation = $( '<div>' );
3325 this.$location = $( '<div>' );
3326 this.$safeActions = $( '<div>' );
3327 this.$primaryActions = $( '<div>' );
3328 this.$otherActions = $( '<div>' );
3329 this.dismissButton = new OO.ui.ButtonWidget( {
3330 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
3331 } );
3332 this.retryButton = new OO.ui.ButtonWidget();
3333 this.$errors = $( '<div>' );
3334 this.$errorsTitle = $( '<div>' );
3335
3336 // Events
3337 this.dismissButton.connect( this, {
3338 click: 'onDismissErrorButtonClick'
3339 } );
3340 this.retryButton.connect( this, {
3341 click: 'onRetryButtonClick'
3342 } );
3343 this.title.connect( this, {
3344 labelChange: 'fitLabel'
3345 } );
3346
3347 // Initialization
3348 this.title.$element.addClass( 'oo-ui-processDialog-title' );
3349 this.$location
3350 .append( this.title.$element )
3351 .addClass( 'oo-ui-processDialog-location' );
3352 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
3353 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
3354 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
3355 this.$errorsTitle
3356 .addClass( 'oo-ui-processDialog-errors-title' )
3357 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
3358 this.$errors
3359 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
3360 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
3361 this.$content
3362 .addClass( 'oo-ui-processDialog-content' )
3363 .append( this.$errors );
3364 this.$navigation
3365 .addClass( 'oo-ui-processDialog-navigation' )
3366 // Note: Order of appends below is important. These are in the order
3367 // we want tab to go through them. Display-order is handled entirely
3368 // by CSS absolute-positioning. As such, primary actions like "done"
3369 // should go first.
3370 .append( this.$primaryActions, this.$location, this.$safeActions );
3371 this.$head.append( this.$navigation );
3372 this.$foot.append( this.$otherActions );
3373 };
3374
3375 /**
3376 * @inheritdoc
3377 */
3378 OO.ui.ProcessDialog.prototype.getActionWidgetConfig = function ( config ) {
3379 function checkFlag( flag ) {
3380 return config.flags === flag ||
3381 ( Array.isArray( config.flags ) && config.flags.indexOf( flag ) !== -1 );
3382 }
3383
3384 config = $.extend( { framed: true }, config );
3385 if ( checkFlag( 'close' ) ) {
3386 // Change close buttons to icon only.
3387 $.extend( config, {
3388 icon: 'close',
3389 invisibleLabel: true
3390 } );
3391 } else if ( checkFlag( 'back' ) ) {
3392 // Change back buttons to icon only.
3393 $.extend( config, {
3394 icon: 'previous',
3395 invisibleLabel: true
3396 } );
3397 }
3398
3399 return config;
3400 };
3401
3402 /**
3403 * @inheritdoc
3404 */
3405 OO.ui.ProcessDialog.prototype.attachActions = function () {
3406 var i, len, other, special, others;
3407
3408 // Parent method
3409 OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
3410
3411 special = this.actions.getSpecial();
3412 others = this.actions.getOthers();
3413 if ( special.primary ) {
3414 this.$primaryActions.append( special.primary.$element );
3415 }
3416 for ( i = 0, len = others.length; i < len; i++ ) {
3417 other = others[ i ];
3418 this.$otherActions.append( other.$element );
3419 }
3420 if ( special.safe ) {
3421 this.$safeActions.append( special.safe.$element );
3422 }
3423 };
3424
3425 /**
3426 * @inheritdoc
3427 */
3428 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
3429 var process = this;
3430 return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
3431 .fail( function ( errors ) {
3432 process.showErrors( errors || [] );
3433 } );
3434 };
3435
3436 /**
3437 * @inheritdoc
3438 */
3439 OO.ui.ProcessDialog.prototype.setDimensions = function () {
3440 var dialog = this;
3441
3442 // Parent method
3443 OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments );
3444
3445 this.fitLabel();
3446
3447 // If there are many actions, they might be shown on multiple lines. Their layout can change
3448 // when resizing the dialog and when changing the actions. Adjust the height of the footer to
3449 // fit them.
3450 dialog.$body.css( 'bottom', dialog.$foot.outerHeight( true ) );
3451 // Wait for CSS transition to finish and do it again :(
3452 setTimeout( function () {
3453 dialog.$body.css( 'bottom', dialog.$foot.outerHeight( true ) );
3454 }, 300 );
3455 };
3456
3457 /**
3458 * Fit label between actions.
3459 *
3460 * @private
3461 * @chainable
3462 * @return {OO.ui.MessageDialog} The dialog, for chaining
3463 */
3464 OO.ui.ProcessDialog.prototype.fitLabel = function () {
3465 var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth,
3466 size = this.getSizeProperties();
3467
3468 if ( typeof size.width !== 'number' ) {
3469 if ( this.isOpened() ) {
3470 navigationWidth = this.$head.width() - 20;
3471 } else if ( this.isOpening() ) {
3472 if ( !this.fitOnOpen ) {
3473 // Size is relative and the dialog isn't open yet, so wait.
3474 // FIXME: This should ideally be handled by setup somehow.
3475 this.manager.lifecycle.opened.done( this.fitLabel.bind( this ) );
3476 this.fitOnOpen = true;
3477 }
3478 return;
3479 } else {
3480 return;
3481 }
3482 } else {
3483 navigationWidth = size.width - 20;
3484 }
3485
3486 safeWidth = this.$safeActions.width();
3487 primaryWidth = this.$primaryActions.width();
3488 biggerWidth = Math.max( safeWidth, primaryWidth );
3489
3490 labelWidth = this.title.$element.width();
3491
3492 if ( !OO.ui.isMobile() && 2 * biggerWidth + labelWidth < navigationWidth ) {
3493 // We have enough space to center the label
3494 leftWidth = rightWidth = biggerWidth;
3495 } else {
3496 // Let's hope we at least have enough space not to overlap, because we can't wrap
3497 // the label.
3498 if ( this.getDir() === 'ltr' ) {
3499 leftWidth = safeWidth;
3500 rightWidth = primaryWidth;
3501 } else {
3502 leftWidth = primaryWidth;
3503 rightWidth = safeWidth;
3504 }
3505 }
3506
3507 this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
3508
3509 return this;
3510 };
3511
3512 /**
3513 * Handle errors that occurred during accept or reject processes.
3514 *
3515 * @private
3516 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
3517 */
3518 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
3519 var i, len, $item, actions,
3520 items = [],
3521 abilities = {},
3522 recoverable = true,
3523 warning = false;
3524
3525 if ( errors instanceof OO.ui.Error ) {
3526 errors = [ errors ];
3527 }
3528
3529 for ( i = 0, len = errors.length; i < len; i++ ) {
3530 if ( !errors[ i ].isRecoverable() ) {
3531 recoverable = false;
3532 }
3533 if ( errors[ i ].isWarning() ) {
3534 warning = true;
3535 }
3536 $item = $( '<div>' )
3537 .addClass( 'oo-ui-processDialog-error' )
3538 .append( errors[ i ].getMessage() );
3539 items.push( $item[ 0 ] );
3540 }
3541 this.$errorItems = $( items );
3542 if ( recoverable ) {
3543 abilities[ this.currentAction ] = true;
3544 // Copy the flags from the first matching action.
3545 actions = this.actions.get( { actions: this.currentAction } );
3546 if ( actions.length ) {
3547 this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
3548 }
3549 } else {
3550 abilities[ this.currentAction ] = false;
3551 this.actions.setAbilities( abilities );
3552 }
3553 if ( warning ) {
3554 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
3555 } else {
3556 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
3557 }
3558 this.retryButton.toggle( recoverable );
3559 this.$errorsTitle.after( this.$errorItems );
3560 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
3561 };
3562
3563 /**
3564 * Hide errors.
3565 *
3566 * @private
3567 */
3568 OO.ui.ProcessDialog.prototype.hideErrors = function () {
3569 this.$errors.addClass( 'oo-ui-element-hidden' );
3570 if ( this.$errorItems ) {
3571 this.$errorItems.remove();
3572 this.$errorItems = null;
3573 }
3574 };
3575
3576 /**
3577 * @inheritdoc
3578 */
3579 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
3580 // Parent method
3581 return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
3582 .first( function () {
3583 // Make sure to hide errors.
3584 this.hideErrors();
3585 this.fitOnOpen = false;
3586 }, this );
3587 };
3588
3589 /**
3590 * @class OO.ui
3591 */
3592
3593 /**
3594 * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
3595 * OO.ui.confirm.
3596 *
3597 * @private
3598 * @return {OO.ui.WindowManager}
3599 */
3600 OO.ui.getWindowManager = function () {
3601 if ( !OO.ui.windowManager ) {
3602 OO.ui.windowManager = new OO.ui.WindowManager();
3603 $( document.body ).append( OO.ui.windowManager.$element );
3604 OO.ui.windowManager.addWindows( [ new OO.ui.MessageDialog() ] );
3605 }
3606 return OO.ui.windowManager;
3607 };
3608
3609 /**
3610 * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
3611 * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
3612 * has only one action button, labelled "OK", clicking it will simply close the dialog.
3613 *
3614 * A window manager is created automatically when this function is called for the first time.
3615 *
3616 * @example
3617 * OO.ui.alert( 'Something happened!' ).done( function () {
3618 * console.log( 'User closed the dialog.' );
3619 * } );
3620 *
3621 * OO.ui.alert( 'Something larger happened!', { size: 'large' } );
3622 *
3623 * @param {jQuery|string} text Message text to display
3624 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3625 * @return {jQuery.Promise} Promise resolved when the user closes the dialog
3626 */
3627 OO.ui.alert = function ( text, options ) {
3628 return OO.ui.getWindowManager().openWindow( 'message', $.extend( {
3629 message: text,
3630 actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
3631 }, options ) ).closed.then( function () {
3632 return undefined;
3633 } );
3634 };
3635
3636 /**
3637 * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
3638 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3639 * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
3640 * (labelled "Cancel").
3641 *
3642 * A window manager is created automatically when this function is called for the first time.
3643 *
3644 * @example
3645 * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
3646 * if ( confirmed ) {
3647 * console.log( 'User clicked "OK"!' );
3648 * } else {
3649 * console.log( 'User clicked "Cancel" or closed the dialog.' );
3650 * }
3651 * } );
3652 *
3653 * @param {jQuery|string} text Message text to display
3654 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3655 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3656 * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
3657 * `false`.
3658 */
3659 OO.ui.confirm = function ( text, options ) {
3660 return OO.ui.getWindowManager().openWindow( 'message', $.extend( {
3661 message: text
3662 }, options ) ).closed.then( function ( data ) {
3663 return !!( data && data.action === 'accept' );
3664 } );
3665 };
3666
3667 /**
3668 * Display a quick modal prompt dialog, using a OO.ui.MessageDialog. While the dialog is open,
3669 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3670 * dialog has a text input widget and two action buttons, one to confirm an operation
3671 * (labelled "OK") and one to cancel it (labelled "Cancel").
3672 *
3673 * A window manager is created automatically when this function is called for the first time.
3674 *
3675 * @example
3676 * OO.ui.prompt( 'Choose a line to go to', {
3677 * textInput: { placeholder: 'Line number' }
3678 * } ).done( function ( result ) {
3679 * if ( result !== null ) {
3680 * console.log( 'User typed "' + result + '" then clicked "OK".' );
3681 * } else {
3682 * console.log( 'User clicked "Cancel" or closed the dialog.' );
3683 * }
3684 * } );
3685 *
3686 * @param {jQuery|string} text Message text to display
3687 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3688 * @param {Object} [options.textInput] Additional options for text input widget,
3689 * see OO.ui.TextInputWidget
3690 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3691 * confirm, the promise will resolve with the value of the text input widget; otherwise, it will
3692 * resolve to `null`.
3693 */
3694 OO.ui.prompt = function ( text, options ) {
3695 var instance,
3696 manager = OO.ui.getWindowManager(),
3697 textInput = new OO.ui.TextInputWidget( ( options && options.textInput ) || {} ),
3698 textField = new OO.ui.FieldLayout( textInput, {
3699 align: 'top',
3700 label: text
3701 } );
3702
3703 instance = manager.openWindow( 'message', $.extend( {
3704 message: textField.$element
3705 }, options ) );
3706
3707 // TODO: This is a little hacky, and could be done by extending MessageDialog instead.
3708 instance.opened.then( function () {
3709 textInput.on( 'enter', function () {
3710 manager.getCurrentWindow().close( { action: 'accept' } );
3711 } );
3712 textInput.focus();
3713 } );
3714
3715 return instance.closed.then( function ( data ) {
3716 return data && data.action === 'accept' ? textInput.getValue() : null;
3717 } );
3718 };
3719
3720 }( OO ) );
3721
3722 //# sourceMappingURL=oojs-ui-windows.js.map.json