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