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