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