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