Merge "resourceloader: Fold legacy modules into base modules request"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui-windows.js
1 /*!
2 * OOjs UI v0.21.0
3 * https://www.mediawiki.org/wiki/OOjs_UI
4 *
5 * Copyright 2011–2017 OOjs UI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2017-04-11T22:51:05Z
10 */
11 ( function ( OO ) {
12
13 'use strict';
14
15 /**
16 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
17 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
18 * of the actions.
19 *
20 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
21 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
22 * and examples.
23 *
24 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
25 *
26 * @class
27 * @extends OO.ui.ButtonWidget
28 * @mixins OO.ui.mixin.PendingElement
29 *
30 * @constructor
31 * @param {Object} [config] Configuration options
32 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
33 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
34 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
35 * for more information about setting modes.
36 * @cfg {boolean} [framed=false] Render the action button with a frame
37 */
38 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
39 // Configuration initialization
40 config = $.extend( { framed: false }, config );
41
42 // Parent constructor
43 OO.ui.ActionWidget.parent.call( this, config );
44
45 // Mixin constructors
46 OO.ui.mixin.PendingElement.call( this, config );
47
48 // Properties
49 this.action = config.action || '';
50 this.modes = config.modes || [];
51 this.width = 0;
52 this.height = 0;
53
54 // Initialization
55 this.$element.addClass( 'oo-ui-actionWidget' );
56 };
57
58 /* Setup */
59
60 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
61 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
62
63 /* 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 * Please see the [OOjs UI 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', 'constructive' ] },
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/OOjs_UI/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 [OOjs UI documentation on MediaWiki][2] for more information and examples.
211 *
212 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/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 [OOjs UI documentation on MediaWiki][1].
609 *
610 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/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 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
856 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
857 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
858 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
859 * pertinent data and reused.
860 *
861 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
862 * `opened`, and `closing`, which represent the primary stages of the cycle:
863 *
864 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
865 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
866 *
867 * - an `opening` event is emitted with an `opening` promise
868 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
869 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
870 * window and its result executed
871 * - a `setup` progress notification is emitted from the `opening` promise
872 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
873 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
874 * window and its result executed
875 * - a `ready` progress notification is emitted from the `opening` promise
876 * - the `opening` promise is resolved with an `opened` promise
877 *
878 * **Opened**: the window is now open.
879 *
880 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
881 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
882 * to close the window.
883 *
884 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
885 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
886 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
887 * window and its result executed
888 * - a `hold` progress notification is emitted from the `closing` promise
889 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
890 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
891 * window and its result executed
892 * - a `teardown` progress notification is emitted from the `closing` promise
893 * - the `closing` promise is resolved. The window is now closed
894 *
895 * See the [OOjs UI documentation on MediaWiki][1] for more information.
896 *
897 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
898 *
899 * @class
900 * @extends OO.ui.Element
901 * @mixins OO.EventEmitter
902 *
903 * @constructor
904 * @param {Object} [config] Configuration options
905 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
906 * Note that window classes that are instantiated with a factory must have
907 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
908 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
909 */
910 OO.ui.WindowManager = function OoUiWindowManager( config ) {
911 // Configuration initialization
912 config = config || {};
913
914 // Parent constructor
915 OO.ui.WindowManager.parent.call( this, config );
916
917 // Mixin constructors
918 OO.EventEmitter.call( this );
919
920 // Properties
921 this.factory = config.factory;
922 this.modal = config.modal === undefined || !!config.modal;
923 this.windows = {};
924 this.opening = null;
925 this.opened = null;
926 this.closing = null;
927 this.preparingToOpen = null;
928 this.preparingToClose = null;
929 this.currentWindow = null;
930 this.globalEvents = false;
931 this.$returnFocusTo = null;
932 this.$ariaHidden = null;
933 this.onWindowResizeTimeout = null;
934 this.onWindowResizeHandler = this.onWindowResize.bind( this );
935 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
936
937 // Initialization
938 this.$element
939 .addClass( 'oo-ui-windowManager' )
940 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
941 };
942
943 /* Setup */
944
945 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
946 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
947
948 /* Events */
949
950 /**
951 * An 'opening' event is emitted when the window begins to be opened.
952 *
953 * @event opening
954 * @param {OO.ui.Window} win Window that's being opened
955 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
956 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
957 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
958 * @param {Object} data Window opening data
959 */
960
961 /**
962 * A 'closing' event is emitted when the window begins to be closed.
963 *
964 * @event closing
965 * @param {OO.ui.Window} win Window that's being closed
966 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
967 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
968 * processes are complete. When the `closing` promise is resolved, the first argument of its value
969 * is the closing data.
970 * @param {Object} data Window closing data
971 */
972
973 /**
974 * A 'resize' event is emitted when a window is resized.
975 *
976 * @event resize
977 * @param {OO.ui.Window} win Window that was resized
978 */
979
980 /* Static Properties */
981
982 /**
983 * Map of the symbolic name of each window size and its CSS properties.
984 *
985 * @static
986 * @inheritable
987 * @property {Object}
988 */
989 OO.ui.WindowManager.static.sizes = {
990 small: {
991 width: 300
992 },
993 medium: {
994 width: 500
995 },
996 large: {
997 width: 700
998 },
999 larger: {
1000 width: 900
1001 },
1002 full: {
1003 // These can be non-numeric because they are never used in calculations
1004 width: '100%',
1005 height: '100%'
1006 }
1007 };
1008
1009 /**
1010 * Symbolic name of the default window size.
1011 *
1012 * The default size is used if the window's requested size is not recognized.
1013 *
1014 * @static
1015 * @inheritable
1016 * @property {string}
1017 */
1018 OO.ui.WindowManager.static.defaultSize = 'medium';
1019
1020 /* Methods */
1021
1022 /**
1023 * Handle window resize events.
1024 *
1025 * @private
1026 * @param {jQuery.Event} e Window resize event
1027 */
1028 OO.ui.WindowManager.prototype.onWindowResize = function () {
1029 clearTimeout( this.onWindowResizeTimeout );
1030 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
1031 };
1032
1033 /**
1034 * Handle window resize events.
1035 *
1036 * @private
1037 * @param {jQuery.Event} e Window resize event
1038 */
1039 OO.ui.WindowManager.prototype.afterWindowResize = function () {
1040 if ( this.currentWindow ) {
1041 this.updateWindowSize( this.currentWindow );
1042 }
1043 };
1044
1045 /**
1046 * Check if window is opening.
1047 *
1048 * @param {OO.ui.Window} win Window to check
1049 * @return {boolean} Window is opening
1050 */
1051 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
1052 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
1053 };
1054
1055 /**
1056 * Check if window is closing.
1057 *
1058 * @param {OO.ui.Window} win Window to check
1059 * @return {boolean} Window is closing
1060 */
1061 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
1062 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
1063 };
1064
1065 /**
1066 * Check if window is opened.
1067 *
1068 * @param {OO.ui.Window} win Window to check
1069 * @return {boolean} Window is opened
1070 */
1071 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
1072 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
1073 };
1074
1075 /**
1076 * Check if a window is being managed.
1077 *
1078 * @param {OO.ui.Window} win Window to check
1079 * @return {boolean} Window is being managed
1080 */
1081 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
1082 var name;
1083
1084 for ( name in this.windows ) {
1085 if ( this.windows[ name ] === win ) {
1086 return true;
1087 }
1088 }
1089
1090 return false;
1091 };
1092
1093 /**
1094 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
1095 *
1096 * @param {OO.ui.Window} win Window being opened
1097 * @param {Object} [data] Window opening data
1098 * @return {number} Milliseconds to wait
1099 */
1100 OO.ui.WindowManager.prototype.getSetupDelay = function () {
1101 return 0;
1102 };
1103
1104 /**
1105 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
1106 *
1107 * @param {OO.ui.Window} win Window being opened
1108 * @param {Object} [data] Window opening data
1109 * @return {number} Milliseconds to wait
1110 */
1111 OO.ui.WindowManager.prototype.getReadyDelay = function () {
1112 return 0;
1113 };
1114
1115 /**
1116 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
1117 *
1118 * @param {OO.ui.Window} win Window being closed
1119 * @param {Object} [data] Window closing data
1120 * @return {number} Milliseconds to wait
1121 */
1122 OO.ui.WindowManager.prototype.getHoldDelay = function () {
1123 return 0;
1124 };
1125
1126 /**
1127 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
1128 * executing the ‘teardown’ process.
1129 *
1130 * @param {OO.ui.Window} win Window being closed
1131 * @param {Object} [data] Window closing data
1132 * @return {number} Milliseconds to wait
1133 */
1134 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
1135 return this.modal ? 250 : 0;
1136 };
1137
1138 /**
1139 * Get a window by its symbolic name.
1140 *
1141 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
1142 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
1143 * for more information about using factories.
1144 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
1145 *
1146 * @param {string} name Symbolic name of the window
1147 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
1148 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
1149 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
1150 */
1151 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
1152 var deferred = $.Deferred(),
1153 win = this.windows[ name ];
1154
1155 if ( !( win instanceof OO.ui.Window ) ) {
1156 if ( this.factory ) {
1157 if ( !this.factory.lookup( name ) ) {
1158 deferred.reject( new OO.ui.Error(
1159 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
1160 ) );
1161 } else {
1162 win = this.factory.create( name );
1163 this.addWindows( [ win ] );
1164 deferred.resolve( win );
1165 }
1166 } else {
1167 deferred.reject( new OO.ui.Error(
1168 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
1169 ) );
1170 }
1171 } else {
1172 deferred.resolve( win );
1173 }
1174
1175 return deferred.promise();
1176 };
1177
1178 /**
1179 * Get current window.
1180 *
1181 * @return {OO.ui.Window|null} Currently opening/opened/closing window
1182 */
1183 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
1184 return this.currentWindow;
1185 };
1186
1187 /**
1188 * Open a window.
1189 *
1190 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
1191 * @param {Object} [data] Window opening data
1192 * @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when closed.
1193 * Defaults the current activeElement. If set to null, focus isn't changed on close.
1194 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
1195 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
1196 * @fires opening
1197 */
1198 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
1199 var manager = this,
1200 opening = $.Deferred();
1201 data = data || {};
1202
1203 // Argument handling
1204 if ( typeof win === 'string' ) {
1205 return this.getWindow( win ).then( function ( win ) {
1206 return manager.openWindow( win, data );
1207 } );
1208 }
1209
1210 // Error handling
1211 if ( !this.hasWindow( win ) ) {
1212 opening.reject( new OO.ui.Error(
1213 'Cannot open window: window is not attached to manager'
1214 ) );
1215 } else if ( this.preparingToOpen || this.opening || this.opened ) {
1216 opening.reject( new OO.ui.Error(
1217 'Cannot open window: another window is opening or open'
1218 ) );
1219 }
1220
1221 // Window opening
1222 if ( opening.state() !== 'rejected' ) {
1223 // If a window is currently closing, wait for it to complete
1224 this.preparingToOpen = $.when( this.closing );
1225 // Ensure handlers get called after preparingToOpen is set
1226 this.preparingToOpen.done( function () {
1227 if ( manager.modal ) {
1228 manager.toggleGlobalEvents( true );
1229 manager.toggleAriaIsolation( true );
1230 }
1231 manager.$returnFocusTo = data.$returnFocusTo !== undefined ? data.$returnFocusTo : $( document.activeElement );
1232 manager.currentWindow = win;
1233 manager.opening = opening;
1234 manager.preparingToOpen = null;
1235 manager.emit( 'opening', win, opening, data );
1236 setTimeout( function () {
1237 win.setup( data ).then( function () {
1238 manager.updateWindowSize( win );
1239 manager.opening.notify( { state: 'setup' } );
1240 setTimeout( function () {
1241 win.ready( data ).then( function () {
1242 manager.opening.notify( { state: 'ready' } );
1243 manager.opening = null;
1244 manager.opened = $.Deferred();
1245 opening.resolve( manager.opened.promise(), data );
1246 }, function () {
1247 manager.opening = null;
1248 manager.opened = $.Deferred();
1249 opening.reject();
1250 manager.closeWindow( win );
1251 } );
1252 }, manager.getReadyDelay() );
1253 }, function () {
1254 manager.opening = null;
1255 manager.opened = $.Deferred();
1256 opening.reject();
1257 manager.closeWindow( win );
1258 } );
1259 }, manager.getSetupDelay() );
1260 } );
1261 }
1262
1263 return opening.promise();
1264 };
1265
1266 /**
1267 * Close a window.
1268 *
1269 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
1270 * @param {Object} [data] Window closing data
1271 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
1272 * See {@link #event-closing 'closing' event} for more information about closing promises.
1273 * @throws {Error} An error is thrown if the window is not managed by the window manager.
1274 * @fires closing
1275 */
1276 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
1277 var manager = this,
1278 closing = $.Deferred(),
1279 opened;
1280
1281 // Argument handling
1282 if ( typeof win === 'string' ) {
1283 win = this.windows[ win ];
1284 } else if ( !this.hasWindow( win ) ) {
1285 win = null;
1286 }
1287
1288 // Error handling
1289 if ( !win ) {
1290 closing.reject( new OO.ui.Error(
1291 'Cannot close window: window is not attached to manager'
1292 ) );
1293 } else if ( win !== this.currentWindow ) {
1294 closing.reject( new OO.ui.Error(
1295 'Cannot close window: window already closed with different data'
1296 ) );
1297 } else if ( this.preparingToClose || this.closing ) {
1298 closing.reject( new OO.ui.Error(
1299 'Cannot close window: window already closing with different data'
1300 ) );
1301 }
1302
1303 // Window closing
1304 if ( closing.state() !== 'rejected' ) {
1305 // If the window is currently opening, close it when it's done
1306 this.preparingToClose = $.when( this.opening );
1307 // Ensure handlers get called after preparingToClose is set
1308 this.preparingToClose.always( function () {
1309 manager.closing = closing;
1310 manager.preparingToClose = null;
1311 manager.emit( 'closing', win, closing, data );
1312 opened = manager.opened;
1313 manager.opened = null;
1314 opened.resolve( closing.promise(), data );
1315 setTimeout( function () {
1316 win.hold( data ).then( function () {
1317 closing.notify( { state: 'hold' } );
1318 setTimeout( function () {
1319 win.teardown( data ).then( function () {
1320 closing.notify( { state: 'teardown' } );
1321 if ( manager.modal ) {
1322 manager.toggleGlobalEvents( false );
1323 manager.toggleAriaIsolation( false );
1324 }
1325 if ( manager.$returnFocusTo && manager.$returnFocusTo.length ) {
1326 manager.$returnFocusTo[ 0 ].focus();
1327 }
1328 manager.closing = null;
1329 manager.currentWindow = null;
1330 closing.resolve( data );
1331 } );
1332 }, manager.getTeardownDelay() );
1333 } );
1334 }, manager.getHoldDelay() );
1335 } );
1336 }
1337
1338 return closing.promise();
1339 };
1340
1341 /**
1342 * Add windows to the window manager.
1343 *
1344 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
1345 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
1346 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
1347 *
1348 * This function can be called in two manners:
1349 *
1350 * 1. `.addWindows( [ windowA, windowB, ... ] )` (where `windowA`, `windowB` are OO.ui.Window objects)
1351 *
1352 * This syntax registers windows under the symbolic names defined in their `.static.name`
1353 * properties. For example, if `windowA.constructor.static.name` is `'nameA'`, calling
1354 * `.openWindow( 'nameA' )` afterwards will open the window `windowA`. This syntax requires the
1355 * static name to be set, otherwise an exception will be thrown.
1356 *
1357 * This is the recommended way, as it allows for an easier switch to using a window factory.
1358 *
1359 * 2. `.addWindows( { nameA: windowA, nameB: windowB, ... } )`
1360 *
1361 * This syntax registers windows under the explicitly given symbolic names. In this example,
1362 * calling `.openWindow( 'nameA' )` afterwards will open the window `windowA`, regardless of what
1363 * its `.static.name` is set to. The static name is not required to be set.
1364 *
1365 * This should only be used if you need to override the default symbolic names.
1366 *
1367 * Example:
1368 *
1369 * var windowManager = new OO.ui.WindowManager();
1370 * $( 'body' ).append( windowManager.$element );
1371 *
1372 * // Add a window under the default name: see OO.ui.MessageDialog.static.name
1373 * windowManager.addWindows( [ new OO.ui.MessageDialog() ] );
1374 * // Add a window under an explicit name
1375 * windowManager.addWindows( { myMessageDialog: new OO.ui.MessageDialog() } );
1376 *
1377 * // Open window by default name
1378 * windowManager.openWindow( 'message' );
1379 * // Open window by explicitly given name
1380 * windowManager.openWindow( 'myMessageDialog' );
1381 *
1382 *
1383 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
1384 * by reference, symbolic name, or explicitly defined symbolic names.
1385 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
1386 * explicit nor a statically configured symbolic name.
1387 */
1388 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
1389 var i, len, win, name, list;
1390
1391 if ( Array.isArray( windows ) ) {
1392 // Convert to map of windows by looking up symbolic names from static configuration
1393 list = {};
1394 for ( i = 0, len = windows.length; i < len; i++ ) {
1395 name = windows[ i ].constructor.static.name;
1396 if ( !name ) {
1397 throw new Error( 'Windows must have a `name` static property defined.' );
1398 }
1399 list[ name ] = windows[ i ];
1400 }
1401 } else if ( OO.isPlainObject( windows ) ) {
1402 list = windows;
1403 }
1404
1405 // Add windows
1406 for ( name in list ) {
1407 win = list[ name ];
1408 this.windows[ name ] = win.toggle( false );
1409 this.$element.append( win.$element );
1410 win.setManager( this );
1411 }
1412 };
1413
1414 /**
1415 * Remove the specified windows from the windows manager.
1416 *
1417 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
1418 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
1419 * longer listens to events, use the #destroy method.
1420 *
1421 * @param {string[]} names Symbolic names of windows to remove
1422 * @return {jQuery.Promise} Promise resolved when window is closed and removed
1423 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
1424 */
1425 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
1426 var i, len, win, name, cleanupWindow,
1427 manager = this,
1428 promises = [],
1429 cleanup = function ( name, win ) {
1430 delete manager.windows[ name ];
1431 win.$element.detach();
1432 };
1433
1434 for ( i = 0, len = names.length; i < len; i++ ) {
1435 name = names[ i ];
1436 win = this.windows[ name ];
1437 if ( !win ) {
1438 throw new Error( 'Cannot remove window' );
1439 }
1440 cleanupWindow = cleanup.bind( null, name, win );
1441 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
1442 }
1443
1444 return $.when.apply( $, promises );
1445 };
1446
1447 /**
1448 * Remove all windows from the window manager.
1449 *
1450 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
1451 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
1452 * To remove just a subset of windows, use the #removeWindows method.
1453 *
1454 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
1455 */
1456 OO.ui.WindowManager.prototype.clearWindows = function () {
1457 return this.removeWindows( Object.keys( this.windows ) );
1458 };
1459
1460 /**
1461 * Set dialog size. In general, this method should not be called directly.
1462 *
1463 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
1464 *
1465 * @param {OO.ui.Window} win Window to update, should be the current window
1466 * @chainable
1467 */
1468 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
1469 var isFullscreen;
1470
1471 // Bypass for non-current, and thus invisible, windows
1472 if ( win !== this.currentWindow ) {
1473 return;
1474 }
1475
1476 isFullscreen = win.getSize() === 'full';
1477
1478 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
1479 this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
1480 win.setDimensions( win.getSizeProperties() );
1481
1482 this.emit( 'resize', win );
1483
1484 return this;
1485 };
1486
1487 /**
1488 * Bind or unbind global events for scrolling.
1489 *
1490 * @private
1491 * @param {boolean} [on] Bind global events
1492 * @chainable
1493 */
1494 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
1495 var scrollWidth, bodyMargin,
1496 $body = $( this.getElementDocument().body ),
1497 // We could have multiple window managers open so only modify
1498 // the body css at the bottom of the stack
1499 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0;
1500
1501 on = on === undefined ? !!this.globalEvents : !!on;
1502
1503 if ( on ) {
1504 if ( !this.globalEvents ) {
1505 $( this.getElementWindow() ).on( {
1506 // Start listening for top-level window dimension changes
1507 'orientationchange resize': this.onWindowResizeHandler
1508 } );
1509 if ( stackDepth === 0 ) {
1510 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
1511 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
1512 $body.css( {
1513 overflow: 'hidden',
1514 'margin-right': bodyMargin + scrollWidth
1515 } );
1516 }
1517 stackDepth++;
1518 this.globalEvents = true;
1519 }
1520 } else if ( this.globalEvents ) {
1521 $( this.getElementWindow() ).off( {
1522 // Stop listening for top-level window dimension changes
1523 'orientationchange resize': this.onWindowResizeHandler
1524 } );
1525 stackDepth--;
1526 if ( stackDepth === 0 ) {
1527 $body.css( {
1528 overflow: '',
1529 'margin-right': ''
1530 } );
1531 }
1532 this.globalEvents = false;
1533 }
1534 $body.data( 'windowManagerGlobalEvents', stackDepth );
1535
1536 return this;
1537 };
1538
1539 /**
1540 * Toggle screen reader visibility of content other than the window manager.
1541 *
1542 * @private
1543 * @param {boolean} [isolate] Make only the window manager visible to screen readers
1544 * @chainable
1545 */
1546 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
1547 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
1548
1549 if ( isolate ) {
1550 if ( !this.$ariaHidden ) {
1551 // Hide everything other than the window manager from screen readers
1552 this.$ariaHidden = $( 'body' )
1553 .children()
1554 .not( this.$element.parentsUntil( 'body' ).last() )
1555 .attr( 'aria-hidden', '' );
1556 }
1557 } else if ( this.$ariaHidden ) {
1558 // Restore screen reader visibility
1559 this.$ariaHidden.removeAttr( 'aria-hidden' );
1560 this.$ariaHidden = null;
1561 }
1562
1563 return this;
1564 };
1565
1566 /**
1567 * Destroy the window manager.
1568 *
1569 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
1570 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
1571 * instead.
1572 */
1573 OO.ui.WindowManager.prototype.destroy = function () {
1574 this.toggleGlobalEvents( false );
1575 this.toggleAriaIsolation( false );
1576 this.clearWindows();
1577 this.$element.remove();
1578 };
1579
1580 /**
1581 * A window is a container for elements that are in a child frame. They are used with
1582 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1583 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1584 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1585 * the window manager will choose a sensible fallback.
1586 *
1587 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1588 * different processes are executed:
1589 *
1590 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1591 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1592 * the window.
1593 *
1594 * - {@link #getSetupProcess} method is called and its result executed
1595 * - {@link #getReadyProcess} method is called and its result executed
1596 *
1597 * **opened**: The window is now open
1598 *
1599 * **closing**: The closing stage begins when the window manager's
1600 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1601 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1602 *
1603 * - {@link #getHoldProcess} method is called and its result executed
1604 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1605 *
1606 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1607 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1608 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1609 * processing can complete. Always assume window processes are executed asynchronously.
1610 *
1611 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1612 *
1613 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1614 *
1615 * @abstract
1616 * @class
1617 * @extends OO.ui.Element
1618 * @mixins OO.EventEmitter
1619 *
1620 * @constructor
1621 * @param {Object} [config] Configuration options
1622 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1623 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
1624 */
1625 OO.ui.Window = function OoUiWindow( config ) {
1626 // Configuration initialization
1627 config = config || {};
1628
1629 // Parent constructor
1630 OO.ui.Window.parent.call( this, config );
1631
1632 // Mixin constructors
1633 OO.EventEmitter.call( this );
1634
1635 // Properties
1636 this.manager = null;
1637 this.size = config.size || this.constructor.static.size;
1638 this.$frame = $( '<div>' );
1639 this.$overlay = $( '<div>' );
1640 this.$content = $( '<div>' );
1641
1642 this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
1643 this.$focusTrapAfter = $( '<div>' ).prop( 'tabIndex', 0 );
1644 this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
1645
1646 // Initialization
1647 this.$overlay.addClass( 'oo-ui-window-overlay' );
1648 this.$content
1649 .addClass( 'oo-ui-window-content' )
1650 .attr( 'tabindex', 0 );
1651 this.$frame
1652 .addClass( 'oo-ui-window-frame' )
1653 .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
1654
1655 this.$element
1656 .addClass( 'oo-ui-window' )
1657 .append( this.$frame, this.$overlay );
1658
1659 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1660 // that reference properties not initialized at that time of parent class construction
1661 // TODO: Find a better way to handle post-constructor setup
1662 this.visible = false;
1663 this.$element.addClass( 'oo-ui-element-hidden' );
1664 };
1665
1666 /* Setup */
1667
1668 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1669 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1670
1671 /* Static Properties */
1672
1673 /**
1674 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1675 *
1676 * The static size is used if no #size is configured during construction.
1677 *
1678 * @static
1679 * @inheritable
1680 * @property {string}
1681 */
1682 OO.ui.Window.static.size = 'medium';
1683
1684 /* Methods */
1685
1686 /**
1687 * Handle mouse down events.
1688 *
1689 * @private
1690 * @param {jQuery.Event} e Mouse down event
1691 */
1692 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1693 // Prevent clicking on the click-block from stealing focus
1694 if ( e.target === this.$element[ 0 ] ) {
1695 return false;
1696 }
1697 };
1698
1699 /**
1700 * Check if the window has been initialized.
1701 *
1702 * Initialization occurs when a window is added to a manager.
1703 *
1704 * @return {boolean} Window has been initialized
1705 */
1706 OO.ui.Window.prototype.isInitialized = function () {
1707 return !!this.manager;
1708 };
1709
1710 /**
1711 * Check if the window is visible.
1712 *
1713 * @return {boolean} Window is visible
1714 */
1715 OO.ui.Window.prototype.isVisible = function () {
1716 return this.visible;
1717 };
1718
1719 /**
1720 * Check if the window is opening.
1721 *
1722 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
1723 * method.
1724 *
1725 * @return {boolean} Window is opening
1726 */
1727 OO.ui.Window.prototype.isOpening = function () {
1728 return this.manager.isOpening( this );
1729 };
1730
1731 /**
1732 * Check if the window is closing.
1733 *
1734 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
1735 *
1736 * @return {boolean} Window is closing
1737 */
1738 OO.ui.Window.prototype.isClosing = function () {
1739 return this.manager.isClosing( this );
1740 };
1741
1742 /**
1743 * Check if the window is opened.
1744 *
1745 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
1746 *
1747 * @return {boolean} Window is opened
1748 */
1749 OO.ui.Window.prototype.isOpened = function () {
1750 return this.manager.isOpened( this );
1751 };
1752
1753 /**
1754 * Get the window manager.
1755 *
1756 * All windows must be attached to a window manager, which is used to open
1757 * and close the window and control its presentation.
1758 *
1759 * @return {OO.ui.WindowManager} Manager of window
1760 */
1761 OO.ui.Window.prototype.getManager = function () {
1762 return this.manager;
1763 };
1764
1765 /**
1766 * Get the symbolic name of the window size (e.g., `small` or `medium`).
1767 *
1768 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
1769 */
1770 OO.ui.Window.prototype.getSize = function () {
1771 var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ),
1772 sizes = this.manager.constructor.static.sizes,
1773 size = this.size;
1774
1775 if ( !sizes[ size ] ) {
1776 size = this.manager.constructor.static.defaultSize;
1777 }
1778 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
1779 size = 'full';
1780 }
1781
1782 return size;
1783 };
1784
1785 /**
1786 * Get the size properties associated with the current window size
1787 *
1788 * @return {Object} Size properties
1789 */
1790 OO.ui.Window.prototype.getSizeProperties = function () {
1791 return this.manager.constructor.static.sizes[ this.getSize() ];
1792 };
1793
1794 /**
1795 * Disable transitions on window's frame for the duration of the callback function, then enable them
1796 * back.
1797 *
1798 * @private
1799 * @param {Function} callback Function to call while transitions are disabled
1800 */
1801 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
1802 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1803 // Disable transitions first, otherwise we'll get values from when the window was animating.
1804 // We need to build the transition CSS properties using these specific properties since
1805 // Firefox doesn't return anything useful when asked just for 'transition'.
1806 var oldTransition = this.$frame.css( 'transition-property' ) + ' ' +
1807 this.$frame.css( 'transition-duration' ) + ' ' +
1808 this.$frame.css( 'transition-timing-function' ) + ' ' +
1809 this.$frame.css( 'transition-delay' );
1810
1811 this.$frame.css( 'transition', 'none' );
1812 callback();
1813
1814 // Force reflow to make sure the style changes done inside callback
1815 // really are not transitioned
1816 this.$frame.height();
1817 this.$frame.css( 'transition', oldTransition );
1818 };
1819
1820 /**
1821 * Get the height of the full window contents (i.e., the window head, body and foot together).
1822 *
1823 * What consistitutes the head, body, and foot varies depending on the window type.
1824 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
1825 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
1826 * and special actions in the head, and dialog content in the body.
1827 *
1828 * To get just the height of the dialog body, use the #getBodyHeight method.
1829 *
1830 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
1831 */
1832 OO.ui.Window.prototype.getContentHeight = function () {
1833 var bodyHeight,
1834 win = this,
1835 bodyStyleObj = this.$body[ 0 ].style,
1836 frameStyleObj = this.$frame[ 0 ].style;
1837
1838 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1839 // Disable transitions first, otherwise we'll get values from when the window was animating.
1840 this.withoutSizeTransitions( function () {
1841 var oldHeight = frameStyleObj.height,
1842 oldPosition = bodyStyleObj.position;
1843 frameStyleObj.height = '1px';
1844 // Force body to resize to new width
1845 bodyStyleObj.position = 'relative';
1846 bodyHeight = win.getBodyHeight();
1847 frameStyleObj.height = oldHeight;
1848 bodyStyleObj.position = oldPosition;
1849 } );
1850
1851 return (
1852 // Add buffer for border
1853 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1854 // Use combined heights of children
1855 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1856 );
1857 };
1858
1859 /**
1860 * Get the height of the window body.
1861 *
1862 * To get the height of the full window contents (the window body, head, and foot together),
1863 * use #getContentHeight.
1864 *
1865 * When this function is called, the window will temporarily have been resized
1866 * to height=1px, so .scrollHeight measurements can be taken accurately.
1867 *
1868 * @return {number} Height of the window body in pixels
1869 */
1870 OO.ui.Window.prototype.getBodyHeight = function () {
1871 return this.$body[ 0 ].scrollHeight;
1872 };
1873
1874 /**
1875 * Get the directionality of the frame (right-to-left or left-to-right).
1876 *
1877 * @return {string} Directionality: `'ltr'` or `'rtl'`
1878 */
1879 OO.ui.Window.prototype.getDir = function () {
1880 return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
1881 };
1882
1883 /**
1884 * Get the 'setup' process.
1885 *
1886 * The setup process is used to set up a window for use in a particular context,
1887 * based on the `data` argument. This method is called during the opening phase of the window’s
1888 * lifecycle.
1889 *
1890 * Override this method to add additional steps to the ‘setup’ process the parent method provides
1891 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
1892 * of OO.ui.Process.
1893 *
1894 * To add window content that persists between openings, you may wish to use the #initialize method
1895 * instead.
1896 *
1897 * @param {Object} [data] Window opening data
1898 * @return {OO.ui.Process} Setup process
1899 */
1900 OO.ui.Window.prototype.getSetupProcess = function () {
1901 return new OO.ui.Process();
1902 };
1903
1904 /**
1905 * Get the ‘ready’ process.
1906 *
1907 * The ready process is used to ready a window for use in a particular
1908 * context, based on the `data` argument. This method is called during the opening phase of
1909 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
1910 *
1911 * Override this method to add additional steps to the ‘ready’ process the parent method
1912 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
1913 * methods of OO.ui.Process.
1914 *
1915 * @param {Object} [data] Window opening data
1916 * @return {OO.ui.Process} Ready process
1917 */
1918 OO.ui.Window.prototype.getReadyProcess = function () {
1919 return new OO.ui.Process();
1920 };
1921
1922 /**
1923 * Get the 'hold' process.
1924 *
1925 * The hold process is used to keep a window from being used in a particular context,
1926 * based on the `data` argument. This method is called during the closing phase of the window’s
1927 * lifecycle.
1928 *
1929 * Override this method to add additional steps to the 'hold' process the parent method provides
1930 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
1931 * of OO.ui.Process.
1932 *
1933 * @param {Object} [data] Window closing data
1934 * @return {OO.ui.Process} Hold process
1935 */
1936 OO.ui.Window.prototype.getHoldProcess = function () {
1937 return new OO.ui.Process();
1938 };
1939
1940 /**
1941 * Get the ‘teardown’ process.
1942 *
1943 * The teardown process is used to teardown a window after use. During teardown,
1944 * user interactions within the window are conveyed and the window is closed, based on the `data`
1945 * argument. This method is called during the closing phase of the window’s lifecycle.
1946 *
1947 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
1948 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
1949 * of OO.ui.Process.
1950 *
1951 * @param {Object} [data] Window closing data
1952 * @return {OO.ui.Process} Teardown process
1953 */
1954 OO.ui.Window.prototype.getTeardownProcess = function () {
1955 return new OO.ui.Process();
1956 };
1957
1958 /**
1959 * Set the window manager.
1960 *
1961 * This will cause the window to initialize. Calling it more than once will cause an error.
1962 *
1963 * @param {OO.ui.WindowManager} manager Manager for this window
1964 * @throws {Error} An error is thrown if the method is called more than once
1965 * @chainable
1966 */
1967 OO.ui.Window.prototype.setManager = function ( manager ) {
1968 if ( this.manager ) {
1969 throw new Error( 'Cannot set window manager, window already has a manager' );
1970 }
1971
1972 this.manager = manager;
1973 this.initialize();
1974
1975 return this;
1976 };
1977
1978 /**
1979 * Set the window size by symbolic name (e.g., 'small' or 'medium')
1980 *
1981 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
1982 * `full`
1983 * @chainable
1984 */
1985 OO.ui.Window.prototype.setSize = function ( size ) {
1986 this.size = size;
1987 this.updateSize();
1988 return this;
1989 };
1990
1991 /**
1992 * Update the window size.
1993 *
1994 * @throws {Error} An error is thrown if the window is not attached to a window manager
1995 * @chainable
1996 */
1997 OO.ui.Window.prototype.updateSize = function () {
1998 if ( !this.manager ) {
1999 throw new Error( 'Cannot update window size, must be attached to a manager' );
2000 }
2001
2002 this.manager.updateWindowSize( this );
2003
2004 return this;
2005 };
2006
2007 /**
2008 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2009 * when the window is opening. In general, setDimensions should not be called directly.
2010 *
2011 * To set the size of the window, use the #setSize method.
2012 *
2013 * @param {Object} dim CSS dimension properties
2014 * @param {string|number} [dim.width] Width
2015 * @param {string|number} [dim.minWidth] Minimum width
2016 * @param {string|number} [dim.maxWidth] Maximum width
2017 * @param {string|number} [dim.height] Height, omit to set based on height of contents
2018 * @param {string|number} [dim.minHeight] Minimum height
2019 * @param {string|number} [dim.maxHeight] Maximum height
2020 * @chainable
2021 */
2022 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2023 var height,
2024 win = this,
2025 styleObj = this.$frame[ 0 ].style;
2026
2027 // Calculate the height we need to set using the correct width
2028 if ( dim.height === undefined ) {
2029 this.withoutSizeTransitions( function () {
2030 var oldWidth = styleObj.width;
2031 win.$frame.css( 'width', dim.width || '' );
2032 height = win.getContentHeight();
2033 styleObj.width = oldWidth;
2034 } );
2035 } else {
2036 height = dim.height;
2037 }
2038
2039 this.$frame.css( {
2040 width: dim.width || '',
2041 minWidth: dim.minWidth || '',
2042 maxWidth: dim.maxWidth || '',
2043 height: height || '',
2044 minHeight: dim.minHeight || '',
2045 maxHeight: dim.maxHeight || ''
2046 } );
2047
2048 return this;
2049 };
2050
2051 /**
2052 * Initialize window contents.
2053 *
2054 * Before the window is opened for the first time, #initialize is called so that content that
2055 * persists between openings can be added to the window.
2056 *
2057 * To set up a window with new content each time the window opens, use #getSetupProcess.
2058 *
2059 * @throws {Error} An error is thrown if the window is not attached to a window manager
2060 * @chainable
2061 */
2062 OO.ui.Window.prototype.initialize = function () {
2063 if ( !this.manager ) {
2064 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2065 }
2066
2067 // Properties
2068 this.$head = $( '<div>' );
2069 this.$body = $( '<div>' );
2070 this.$foot = $( '<div>' );
2071 this.$document = $( this.getElementDocument() );
2072
2073 // Events
2074 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2075
2076 // Initialization
2077 this.$head.addClass( 'oo-ui-window-head' );
2078 this.$body.addClass( 'oo-ui-window-body' );
2079 this.$foot.addClass( 'oo-ui-window-foot' );
2080 this.$content.append( this.$head, this.$body, this.$foot );
2081
2082 return this;
2083 };
2084
2085 /**
2086 * Called when someone tries to focus the hidden element at the end of the dialog.
2087 * Sends focus back to the start of the dialog.
2088 *
2089 * @param {jQuery.Event} event Focus event
2090 */
2091 OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
2092 var backwards = this.$focusTrapBefore.is( event.target ),
2093 element = OO.ui.findFocusable( this.$content, backwards );
2094 if ( element ) {
2095 // There's a focusable element inside the content, at the front or
2096 // back depending on which focus trap we hit; select it.
2097 element.focus();
2098 } else {
2099 // There's nothing focusable inside the content. As a fallback,
2100 // this.$content is focusable, and focusing it will keep our focus
2101 // properly trapped. It's not a *meaningful* focus, since it's just
2102 // the content-div for the Window, but it's better than letting focus
2103 // escape into the page.
2104 this.$content.focus();
2105 }
2106 };
2107
2108 /**
2109 * Open the window.
2110 *
2111 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2112 * method, which returns a promise resolved when the window is done opening.
2113 *
2114 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2115 *
2116 * @param {Object} [data] Window opening data
2117 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2118 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2119 * value is a new promise, which is resolved when the window begins closing.
2120 * @throws {Error} An error is thrown if the window is not attached to a window manager
2121 */
2122 OO.ui.Window.prototype.open = function ( data ) {
2123 if ( !this.manager ) {
2124 throw new Error( 'Cannot open window, must be attached to a manager' );
2125 }
2126
2127 return this.manager.openWindow( this, data );
2128 };
2129
2130 /**
2131 * Close the window.
2132 *
2133 * This method is a wrapper around a call to the window
2134 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2135 * which returns a closing promise resolved when the window is done closing.
2136 *
2137 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2138 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2139 * the window closes.
2140 *
2141 * @param {Object} [data] Window closing data
2142 * @return {jQuery.Promise} Promise resolved when window is closed
2143 * @throws {Error} An error is thrown if the window is not attached to a window manager
2144 */
2145 OO.ui.Window.prototype.close = function ( data ) {
2146 if ( !this.manager ) {
2147 throw new Error( 'Cannot close window, must be attached to a manager' );
2148 }
2149
2150 return this.manager.closeWindow( this, data );
2151 };
2152
2153 /**
2154 * Setup window.
2155 *
2156 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2157 * by other systems.
2158 *
2159 * @param {Object} [data] Window opening data
2160 * @return {jQuery.Promise} Promise resolved when window is setup
2161 */
2162 OO.ui.Window.prototype.setup = function ( data ) {
2163 var win = this;
2164
2165 this.toggle( true );
2166
2167 this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
2168 this.$focusTraps.on( 'focus', this.focusTrapHandler );
2169
2170 return this.getSetupProcess( data ).execute().then( function () {
2171 // Force redraw by asking the browser to measure the elements' widths
2172 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2173 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2174 } );
2175 };
2176
2177 /**
2178 * Ready window.
2179 *
2180 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2181 * by other systems.
2182 *
2183 * @param {Object} [data] Window opening data
2184 * @return {jQuery.Promise} Promise resolved when window is ready
2185 */
2186 OO.ui.Window.prototype.ready = function ( data ) {
2187 var win = this;
2188
2189 this.$content.focus();
2190 return this.getReadyProcess( data ).execute().then( function () {
2191 // Force redraw by asking the browser to measure the elements' widths
2192 win.$element.addClass( 'oo-ui-window-ready' ).width();
2193 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2194 } );
2195 };
2196
2197 /**
2198 * Hold window.
2199 *
2200 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2201 * by other systems.
2202 *
2203 * @param {Object} [data] Window closing data
2204 * @return {jQuery.Promise} Promise resolved when window is held
2205 */
2206 OO.ui.Window.prototype.hold = function ( data ) {
2207 var win = this;
2208
2209 return this.getHoldProcess( data ).execute().then( function () {
2210 // Get the focused element within the window's content
2211 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2212
2213 // Blur the focused element
2214 if ( $focus.length ) {
2215 $focus[ 0 ].blur();
2216 }
2217
2218 // Force redraw by asking the browser to measure the elements' widths
2219 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2220 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2221 } );
2222 };
2223
2224 /**
2225 * Teardown window.
2226 *
2227 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2228 * by other systems.
2229 *
2230 * @param {Object} [data] Window closing data
2231 * @return {jQuery.Promise} Promise resolved when window is torn down
2232 */
2233 OO.ui.Window.prototype.teardown = function ( data ) {
2234 var win = this;
2235
2236 return this.getTeardownProcess( data ).execute().then( function () {
2237 // Force redraw by asking the browser to measure the elements' widths
2238 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2239 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2240 win.$focusTraps.off( 'focus', win.focusTrapHandler );
2241 win.toggle( false );
2242 } );
2243 };
2244
2245 /**
2246 * The Dialog class serves as the base class for the other types of dialogs.
2247 * Unless extended to include controls, the rendered dialog box is a simple window
2248 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2249 * which opens, closes, and controls the presentation of the window. See the
2250 * [OOjs UI documentation on MediaWiki] [1] for more information.
2251 *
2252 * @example
2253 * // A simple dialog window.
2254 * function MyDialog( config ) {
2255 * MyDialog.parent.call( this, config );
2256 * }
2257 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2258 * MyDialog.static.name = 'myDialog';
2259 * MyDialog.prototype.initialize = function () {
2260 * MyDialog.parent.prototype.initialize.call( this );
2261 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2262 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2263 * this.$body.append( this.content.$element );
2264 * };
2265 * MyDialog.prototype.getBodyHeight = function () {
2266 * return this.content.$element.outerHeight( true );
2267 * };
2268 * var myDialog = new MyDialog( {
2269 * size: 'medium'
2270 * } );
2271 * // Create and append a window manager, which opens and closes the window.
2272 * var windowManager = new OO.ui.WindowManager();
2273 * $( 'body' ).append( windowManager.$element );
2274 * windowManager.addWindows( [ myDialog ] );
2275 * // Open the window!
2276 * windowManager.openWindow( myDialog );
2277 *
2278 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2279 *
2280 * @abstract
2281 * @class
2282 * @extends OO.ui.Window
2283 * @mixins OO.ui.mixin.PendingElement
2284 *
2285 * @constructor
2286 * @param {Object} [config] Configuration options
2287 */
2288 OO.ui.Dialog = function OoUiDialog( config ) {
2289 // Parent constructor
2290 OO.ui.Dialog.parent.call( this, config );
2291
2292 // Mixin constructors
2293 OO.ui.mixin.PendingElement.call( this );
2294
2295 // Properties
2296 this.actions = new OO.ui.ActionSet();
2297 this.attachedActions = [];
2298 this.currentAction = null;
2299 this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
2300
2301 // Events
2302 this.actions.connect( this, {
2303 click: 'onActionClick',
2304 change: 'onActionsChange'
2305 } );
2306
2307 // Initialization
2308 this.$element
2309 .addClass( 'oo-ui-dialog' )
2310 .attr( 'role', 'dialog' );
2311 };
2312
2313 /* Setup */
2314
2315 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2316 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2317
2318 /* Static Properties */
2319
2320 /**
2321 * Symbolic name of dialog.
2322 *
2323 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2324 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2325 *
2326 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2327 *
2328 * @abstract
2329 * @static
2330 * @inheritable
2331 * @property {string}
2332 */
2333 OO.ui.Dialog.static.name = '';
2334
2335 /**
2336 * The dialog title.
2337 *
2338 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
2339 * that will produce a Label node or string. The title can also be specified with data passed to the
2340 * constructor (see #getSetupProcess). In this case, the static value will be overridden.
2341 *
2342 * @abstract
2343 * @static
2344 * @inheritable
2345 * @property {jQuery|string|Function}
2346 */
2347 OO.ui.Dialog.static.title = '';
2348
2349 /**
2350 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2351 *
2352 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2353 * value will be overridden.
2354 *
2355 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2356 *
2357 * @static
2358 * @inheritable
2359 * @property {Object[]}
2360 */
2361 OO.ui.Dialog.static.actions = [];
2362
2363 /**
2364 * Close the dialog when the 'Esc' key is pressed.
2365 *
2366 * @static
2367 * @abstract
2368 * @inheritable
2369 * @property {boolean}
2370 */
2371 OO.ui.Dialog.static.escapable = true;
2372
2373 /* Methods */
2374
2375 /**
2376 * Handle frame document key down events.
2377 *
2378 * @private
2379 * @param {jQuery.Event} e Key down event
2380 */
2381 OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
2382 var actions;
2383 if ( e.which === OO.ui.Keys.ESCAPE && this.constructor.static.escapable ) {
2384 this.executeAction( '' );
2385 e.preventDefault();
2386 e.stopPropagation();
2387 } else if ( e.which === OO.ui.Keys.ENTER && ( e.ctrlKey || e.metaKey ) ) {
2388 actions = this.actions.get( { flags: 'primary', visible: true, disabled: false } );
2389 if ( actions.length > 0 ) {
2390 this.executeAction( actions[ 0 ].getAction() );
2391 e.preventDefault();
2392 e.stopPropagation();
2393 }
2394 }
2395 };
2396
2397 /**
2398 * Handle action click events.
2399 *
2400 * @private
2401 * @param {OO.ui.ActionWidget} action Action that was clicked
2402 */
2403 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2404 if ( !this.isPending() ) {
2405 this.executeAction( action.getAction() );
2406 }
2407 };
2408
2409 /**
2410 * Handle actions change event.
2411 *
2412 * @private
2413 */
2414 OO.ui.Dialog.prototype.onActionsChange = function () {
2415 this.detachActions();
2416 if ( !this.isClosing() ) {
2417 this.attachActions();
2418 }
2419 };
2420
2421 /**
2422 * Get the set of actions used by the dialog.
2423 *
2424 * @return {OO.ui.ActionSet}
2425 */
2426 OO.ui.Dialog.prototype.getActions = function () {
2427 return this.actions;
2428 };
2429
2430 /**
2431 * Get a process for taking action.
2432 *
2433 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2434 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2435 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2436 *
2437 * @param {string} [action] Symbolic name of action
2438 * @return {OO.ui.Process} Action process
2439 */
2440 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2441 return new OO.ui.Process()
2442 .next( function () {
2443 if ( !action ) {
2444 // An empty action always closes the dialog without data, which should always be
2445 // safe and make no changes
2446 this.close();
2447 }
2448 }, this );
2449 };
2450
2451 /**
2452 * @inheritdoc
2453 *
2454 * @param {Object} [data] Dialog opening data
2455 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2456 * the {@link #static-title static title}
2457 * @param {Object[]} [data.actions] List of configuration options for each
2458 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2459 */
2460 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2461 data = data || {};
2462
2463 // Parent method
2464 return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
2465 .next( function () {
2466 var config = this.constructor.static,
2467 actions = data.actions !== undefined ? data.actions : config.actions,
2468 title = data.title !== undefined ? data.title : config.title;
2469
2470 this.title.setLabel( title ).setTitle( title );
2471 this.actions.add( this.getActionWidgets( actions ) );
2472
2473 this.$element.on( 'keydown', this.onDialogKeyDownHandler );
2474 }, this );
2475 };
2476
2477 /**
2478 * @inheritdoc
2479 */
2480 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2481 // Parent method
2482 return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
2483 .first( function () {
2484 this.$element.off( 'keydown', this.onDialogKeyDownHandler );
2485
2486 this.actions.clear();
2487 this.currentAction = null;
2488 }, this );
2489 };
2490
2491 /**
2492 * @inheritdoc
2493 */
2494 OO.ui.Dialog.prototype.initialize = function () {
2495 var titleId;
2496
2497 // Parent method
2498 OO.ui.Dialog.parent.prototype.initialize.call( this );
2499
2500 titleId = OO.ui.generateElementId();
2501
2502 // Properties
2503 this.title = new OO.ui.LabelWidget( {
2504 id: titleId
2505 } );
2506
2507 // Initialization
2508 this.$content.addClass( 'oo-ui-dialog-content' );
2509 this.$element.attr( 'aria-labelledby', titleId );
2510 this.setPendingElement( this.$head );
2511 };
2512
2513 /**
2514 * Get action widgets from a list of configs
2515 *
2516 * @param {Object[]} actions Action widget configs
2517 * @return {OO.ui.ActionWidget[]} Action widgets
2518 */
2519 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2520 var i, len, widgets = [];
2521 for ( i = 0, len = actions.length; i < len; i++ ) {
2522 widgets.push(
2523 new OO.ui.ActionWidget( actions[ i ] )
2524 );
2525 }
2526 return widgets;
2527 };
2528
2529 /**
2530 * Attach action actions.
2531 *
2532 * @protected
2533 */
2534 OO.ui.Dialog.prototype.attachActions = function () {
2535 // Remember the list of potentially attached actions
2536 this.attachedActions = this.actions.get();
2537 };
2538
2539 /**
2540 * Detach action actions.
2541 *
2542 * @protected
2543 * @chainable
2544 */
2545 OO.ui.Dialog.prototype.detachActions = function () {
2546 var i, len;
2547
2548 // Detach all actions that may have been previously attached
2549 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2550 this.attachedActions[ i ].$element.detach();
2551 }
2552 this.attachedActions = [];
2553 };
2554
2555 /**
2556 * Execute an action.
2557 *
2558 * @param {string} action Symbolic name of action to execute
2559 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2560 */
2561 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2562 this.pushPending();
2563 this.currentAction = action;
2564 return this.getActionProcess( action ).execute()
2565 .always( this.popPending.bind( this ) );
2566 };
2567
2568 /**
2569 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
2570 * consists of a header that contains the dialog title, a body with the message, and a footer that
2571 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
2572 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
2573 *
2574 * There are two basic types of message dialogs, confirmation and alert:
2575 *
2576 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
2577 * more details about the consequences.
2578 * - **alert**: the dialog title describes which event occurred and the message provides more information
2579 * about why the event occurred.
2580 *
2581 * The MessageDialog class specifies two actions: ‘accept’, the primary
2582 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
2583 * passing along the selected action.
2584 *
2585 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
2586 *
2587 * @example
2588 * // Example: Creating and opening a message dialog window.
2589 * var messageDialog = new OO.ui.MessageDialog();
2590 *
2591 * // Create and append a window manager.
2592 * var windowManager = new OO.ui.WindowManager();
2593 * $( 'body' ).append( windowManager.$element );
2594 * windowManager.addWindows( [ messageDialog ] );
2595 * // Open the window.
2596 * windowManager.openWindow( messageDialog, {
2597 * title: 'Basic message dialog',
2598 * message: 'This is the message'
2599 * } );
2600 *
2601 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
2602 *
2603 * @class
2604 * @extends OO.ui.Dialog
2605 *
2606 * @constructor
2607 * @param {Object} [config] Configuration options
2608 */
2609 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
2610 // Parent constructor
2611 OO.ui.MessageDialog.parent.call( this, config );
2612
2613 // Properties
2614 this.verticalActionLayout = null;
2615
2616 // Initialization
2617 this.$element.addClass( 'oo-ui-messageDialog' );
2618 };
2619
2620 /* Setup */
2621
2622 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
2623
2624 /* Static Properties */
2625
2626 /**
2627 * @static
2628 * @inheritdoc
2629 */
2630 OO.ui.MessageDialog.static.name = 'message';
2631
2632 /**
2633 * @static
2634 * @inheritdoc
2635 */
2636 OO.ui.MessageDialog.static.size = 'small';
2637
2638 /**
2639 * Dialog title.
2640 *
2641 * The title of a confirmation dialog describes what a progressive action will do. The
2642 * title of an alert dialog describes which event occurred.
2643 *
2644 * @static
2645 * @inheritable
2646 * @property {jQuery|string|Function|null}
2647 */
2648 OO.ui.MessageDialog.static.title = null;
2649
2650 /**
2651 * The message displayed in the dialog body.
2652 *
2653 * A confirmation message describes the consequences of a progressive action. An alert
2654 * message describes why an event occurred.
2655 *
2656 * @static
2657 * @inheritable
2658 * @property {jQuery|string|Function|null}
2659 */
2660 OO.ui.MessageDialog.static.message = null;
2661
2662 /**
2663 * @static
2664 * @inheritdoc
2665 */
2666 OO.ui.MessageDialog.static.actions = [
2667 // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
2668 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
2669 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
2670 ];
2671
2672 /* Methods */
2673
2674 /**
2675 * @inheritdoc
2676 */
2677 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
2678 OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
2679
2680 // Events
2681 this.manager.connect( this, {
2682 resize: 'onResize'
2683 } );
2684
2685 return this;
2686 };
2687
2688 /**
2689 * Handle window resized events.
2690 *
2691 * @private
2692 */
2693 OO.ui.MessageDialog.prototype.onResize = function () {
2694 var dialog = this;
2695 dialog.fitActions();
2696 // Wait for CSS transition to finish and do it again :(
2697 setTimeout( function () {
2698 dialog.fitActions();
2699 }, 300 );
2700 };
2701
2702 /**
2703 * Toggle action layout between vertical and horizontal.
2704 *
2705 * @private
2706 * @param {boolean} [value] Layout actions vertically, omit to toggle
2707 * @chainable
2708 */
2709 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
2710 value = value === undefined ? !this.verticalActionLayout : !!value;
2711
2712 if ( value !== this.verticalActionLayout ) {
2713 this.verticalActionLayout = value;
2714 this.$actions
2715 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
2716 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
2717 }
2718
2719 return this;
2720 };
2721
2722 /**
2723 * @inheritdoc
2724 */
2725 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
2726 if ( action ) {
2727 return new OO.ui.Process( function () {
2728 this.close( { action: action } );
2729 }, this );
2730 }
2731 return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
2732 };
2733
2734 /**
2735 * @inheritdoc
2736 *
2737 * @param {Object} [data] Dialog opening data
2738 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
2739 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
2740 * @param {string} [data.size] Symbolic name of the dialog size, see OO.ui.Window
2741 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
2742 * action item
2743 */
2744 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
2745 data = data || {};
2746
2747 // Parent method
2748 return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
2749 .next( function () {
2750 this.title.setLabel(
2751 data.title !== undefined ? data.title : this.constructor.static.title
2752 );
2753 this.message.setLabel(
2754 data.message !== undefined ? data.message : this.constructor.static.message
2755 );
2756 this.size = data.size !== undefined ? data.size : this.constructor.static.size;
2757 }, this );
2758 };
2759
2760 /**
2761 * @inheritdoc
2762 */
2763 OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
2764 data = data || {};
2765
2766 // Parent method
2767 return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data )
2768 .next( function () {
2769 // Focus the primary action button
2770 var actions = this.actions.get();
2771 actions = actions.filter( function ( action ) {
2772 return action.getFlags().indexOf( 'primary' ) > -1;
2773 } );
2774 if ( actions.length > 0 ) {
2775 actions[ 0 ].$button.focus();
2776 }
2777 }, this );
2778 };
2779
2780 /**
2781 * @inheritdoc
2782 */
2783 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
2784 var bodyHeight, oldOverflow,
2785 $scrollable = this.container.$element;
2786
2787 oldOverflow = $scrollable[ 0 ].style.overflow;
2788 $scrollable[ 0 ].style.overflow = 'hidden';
2789
2790 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
2791
2792 bodyHeight = this.text.$element.outerHeight( true );
2793 $scrollable[ 0 ].style.overflow = oldOverflow;
2794
2795 return bodyHeight;
2796 };
2797
2798 /**
2799 * @inheritdoc
2800 */
2801 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
2802 var $scrollable = this.container.$element;
2803 OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
2804
2805 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
2806 // Need to do it after transition completes (250ms), add 50ms just in case.
2807 setTimeout( function () {
2808 var oldOverflow = $scrollable[ 0 ].style.overflow,
2809 activeElement = document.activeElement;
2810
2811 $scrollable[ 0 ].style.overflow = 'hidden';
2812
2813 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
2814
2815 // Check reconsiderScrollbars didn't destroy our focus, as we
2816 // are doing this after the ready process.
2817 if ( activeElement && activeElement !== document.activeElement && activeElement.focus ) {
2818 activeElement.focus();
2819 }
2820
2821 $scrollable[ 0 ].style.overflow = oldOverflow;
2822 }, 300 );
2823
2824 return this;
2825 };
2826
2827 /**
2828 * @inheritdoc
2829 */
2830 OO.ui.MessageDialog.prototype.initialize = function () {
2831 // Parent method
2832 OO.ui.MessageDialog.parent.prototype.initialize.call( this );
2833
2834 // Properties
2835 this.$actions = $( '<div>' );
2836 this.container = new OO.ui.PanelLayout( {
2837 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
2838 } );
2839 this.text = new OO.ui.PanelLayout( {
2840 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
2841 } );
2842 this.message = new OO.ui.LabelWidget( {
2843 classes: [ 'oo-ui-messageDialog-message' ]
2844 } );
2845
2846 // Initialization
2847 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
2848 this.$content.addClass( 'oo-ui-messageDialog-content' );
2849 this.container.$element.append( this.text.$element );
2850 this.text.$element.append( this.title.$element, this.message.$element );
2851 this.$body.append( this.container.$element );
2852 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
2853 this.$foot.append( this.$actions );
2854 };
2855
2856 /**
2857 * @inheritdoc
2858 */
2859 OO.ui.MessageDialog.prototype.attachActions = function () {
2860 var i, len, other, special, others;
2861
2862 // Parent method
2863 OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
2864
2865 special = this.actions.getSpecial();
2866 others = this.actions.getOthers();
2867
2868 if ( special.safe ) {
2869 this.$actions.append( special.safe.$element );
2870 special.safe.toggleFramed( false );
2871 }
2872 if ( others.length ) {
2873 for ( i = 0, len = others.length; i < len; i++ ) {
2874 other = others[ i ];
2875 this.$actions.append( other.$element );
2876 other.toggleFramed( false );
2877 }
2878 }
2879 if ( special.primary ) {
2880 this.$actions.append( special.primary.$element );
2881 special.primary.toggleFramed( false );
2882 }
2883
2884 if ( !this.isOpening() ) {
2885 // If the dialog is currently opening, this will be called automatically soon.
2886 // This also calls #fitActions.
2887 this.updateSize();
2888 }
2889 };
2890
2891 /**
2892 * Fit action actions into columns or rows.
2893 *
2894 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
2895 *
2896 * @private
2897 */
2898 OO.ui.MessageDialog.prototype.fitActions = function () {
2899 var i, len, action,
2900 previous = this.verticalActionLayout,
2901 actions = this.actions.get();
2902
2903 // Detect clipping
2904 this.toggleVerticalActionLayout( false );
2905 for ( i = 0, len = actions.length; i < len; i++ ) {
2906 action = actions[ i ];
2907 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
2908 this.toggleVerticalActionLayout( true );
2909 break;
2910 }
2911 }
2912
2913 // Move the body out of the way of the foot
2914 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
2915
2916 if ( this.verticalActionLayout !== previous ) {
2917 // We changed the layout, window height might need to be updated.
2918 this.updateSize();
2919 }
2920 };
2921
2922 /**
2923 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
2924 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
2925 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
2926 * relevant. The ProcessDialog class is always extended and customized with the actions and content
2927 * required for each process.
2928 *
2929 * The process dialog box consists of a header that visually represents the ‘working’ state of long
2930 * processes with an animation. The header contains the dialog title as well as
2931 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
2932 * a ‘primary’ action on the right (e.g., ‘Done’).
2933 *
2934 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
2935 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
2936 *
2937 * @example
2938 * // Example: Creating and opening a process dialog window.
2939 * function MyProcessDialog( config ) {
2940 * MyProcessDialog.parent.call( this, config );
2941 * }
2942 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
2943 *
2944 * MyProcessDialog.static.name = 'myProcessDialog';
2945 * MyProcessDialog.static.title = 'Process dialog';
2946 * MyProcessDialog.static.actions = [
2947 * { action: 'save', label: 'Done', flags: 'primary' },
2948 * { label: 'Cancel', flags: 'safe' }
2949 * ];
2950 *
2951 * MyProcessDialog.prototype.initialize = function () {
2952 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
2953 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2954 * 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>' );
2955 * this.$body.append( this.content.$element );
2956 * };
2957 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
2958 * var dialog = this;
2959 * if ( action ) {
2960 * return new OO.ui.Process( function () {
2961 * dialog.close( { action: action } );
2962 * } );
2963 * }
2964 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
2965 * };
2966 *
2967 * var windowManager = new OO.ui.WindowManager();
2968 * $( 'body' ).append( windowManager.$element );
2969 *
2970 * var dialog = new MyProcessDialog();
2971 * windowManager.addWindows( [ dialog ] );
2972 * windowManager.openWindow( dialog );
2973 *
2974 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
2975 *
2976 * @abstract
2977 * @class
2978 * @extends OO.ui.Dialog
2979 *
2980 * @constructor
2981 * @param {Object} [config] Configuration options
2982 */
2983 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
2984 // Parent constructor
2985 OO.ui.ProcessDialog.parent.call( this, config );
2986
2987 // Properties
2988 this.fitOnOpen = false;
2989
2990 // Initialization
2991 this.$element.addClass( 'oo-ui-processDialog' );
2992 };
2993
2994 /* Setup */
2995
2996 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
2997
2998 /* Methods */
2999
3000 /**
3001 * Handle dismiss button click events.
3002 *
3003 * Hides errors.
3004 *
3005 * @private
3006 */
3007 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
3008 this.hideErrors();
3009 };
3010
3011 /**
3012 * Handle retry button click events.
3013 *
3014 * Hides errors and then tries again.
3015 *
3016 * @private
3017 */
3018 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
3019 this.hideErrors();
3020 this.executeAction( this.currentAction );
3021 };
3022
3023 /**
3024 * @inheritdoc
3025 */
3026 OO.ui.ProcessDialog.prototype.initialize = function () {
3027 // Parent method
3028 OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
3029
3030 // Properties
3031 this.$navigation = $( '<div>' );
3032 this.$location = $( '<div>' );
3033 this.$safeActions = $( '<div>' );
3034 this.$primaryActions = $( '<div>' );
3035 this.$otherActions = $( '<div>' );
3036 this.dismissButton = new OO.ui.ButtonWidget( {
3037 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
3038 } );
3039 this.retryButton = new OO.ui.ButtonWidget();
3040 this.$errors = $( '<div>' );
3041 this.$errorsTitle = $( '<div>' );
3042
3043 // Events
3044 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
3045 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
3046
3047 // Initialization
3048 this.title.$element.addClass( 'oo-ui-processDialog-title' );
3049 this.$location
3050 .append( this.title.$element )
3051 .addClass( 'oo-ui-processDialog-location' );
3052 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
3053 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
3054 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
3055 this.$errorsTitle
3056 .addClass( 'oo-ui-processDialog-errors-title' )
3057 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
3058 this.$errors
3059 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
3060 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
3061 this.$content
3062 .addClass( 'oo-ui-processDialog-content' )
3063 .append( this.$errors );
3064 this.$navigation
3065 .addClass( 'oo-ui-processDialog-navigation' )
3066 // Note: Order of appends below is important. These are in the order
3067 // we want tab to go through them. Display-order is handled entirely
3068 // by CSS absolute-positioning. As such, primary actions like "done"
3069 // should go first.
3070 .append( this.$primaryActions, this.$location, this.$safeActions );
3071 this.$head.append( this.$navigation );
3072 this.$foot.append( this.$otherActions );
3073 };
3074
3075 /**
3076 * @inheritdoc
3077 */
3078 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
3079 var i, len, config,
3080 isMobile = OO.ui.isMobile(),
3081 widgets = [];
3082
3083 for ( i = 0, len = actions.length; i < len; i++ ) {
3084 config = $.extend( { framed: !OO.ui.isMobile() }, actions[ i ] );
3085 if ( isMobile &&
3086 ( config.flags === 'back' || ( Array.isArray( config.flags ) && config.flags.indexOf( 'back' ) !== -1 ) )
3087 ) {
3088 $.extend( config, {
3089 icon: 'previous',
3090 label: ''
3091 } );
3092 }
3093 widgets.push(
3094 new OO.ui.ActionWidget( config )
3095 );
3096 }
3097 return widgets;
3098 };
3099
3100 /**
3101 * @inheritdoc
3102 */
3103 OO.ui.ProcessDialog.prototype.attachActions = function () {
3104 var i, len, other, special, others;
3105
3106 // Parent method
3107 OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
3108
3109 special = this.actions.getSpecial();
3110 others = this.actions.getOthers();
3111 if ( special.primary ) {
3112 this.$primaryActions.append( special.primary.$element );
3113 }
3114 for ( i = 0, len = others.length; i < len; i++ ) {
3115 other = others[ i ];
3116 this.$otherActions.append( other.$element );
3117 }
3118 if ( special.safe ) {
3119 this.$safeActions.append( special.safe.$element );
3120 }
3121
3122 this.fitLabel();
3123 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
3124 };
3125
3126 /**
3127 * @inheritdoc
3128 */
3129 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
3130 var process = this;
3131 return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
3132 .fail( function ( errors ) {
3133 process.showErrors( errors || [] );
3134 } );
3135 };
3136
3137 /**
3138 * @inheritdoc
3139 */
3140 OO.ui.ProcessDialog.prototype.setDimensions = function () {
3141 // Parent method
3142 OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments );
3143
3144 this.fitLabel();
3145 };
3146
3147 /**
3148 * Fit label between actions.
3149 *
3150 * @private
3151 * @chainable
3152 */
3153 OO.ui.ProcessDialog.prototype.fitLabel = function () {
3154 var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth,
3155 size = this.getSizeProperties();
3156
3157 if ( typeof size.width !== 'number' ) {
3158 if ( this.isOpened() ) {
3159 navigationWidth = this.$head.width() - 20;
3160 } else if ( this.isOpening() ) {
3161 if ( !this.fitOnOpen ) {
3162 // Size is relative and the dialog isn't open yet, so wait.
3163 this.manager.opening.done( this.fitLabel.bind( this ) );
3164 this.fitOnOpen = true;
3165 }
3166 return;
3167 } else {
3168 return;
3169 }
3170 } else {
3171 navigationWidth = size.width - 20;
3172 }
3173
3174 safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
3175 primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
3176 biggerWidth = Math.max( safeWidth, primaryWidth );
3177
3178 labelWidth = this.title.$element.width();
3179
3180 if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
3181 // We have enough space to center the label
3182 leftWidth = rightWidth = biggerWidth;
3183 } else {
3184 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
3185 if ( this.getDir() === 'ltr' ) {
3186 leftWidth = safeWidth;
3187 rightWidth = primaryWidth;
3188 } else {
3189 leftWidth = primaryWidth;
3190 rightWidth = safeWidth;
3191 }
3192 }
3193
3194 this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
3195
3196 return this;
3197 };
3198
3199 /**
3200 * Handle errors that occurred during accept or reject processes.
3201 *
3202 * @private
3203 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
3204 */
3205 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
3206 var i, len, $item, actions,
3207 items = [],
3208 abilities = {},
3209 recoverable = true,
3210 warning = false;
3211
3212 if ( errors instanceof OO.ui.Error ) {
3213 errors = [ errors ];
3214 }
3215
3216 for ( i = 0, len = errors.length; i < len; i++ ) {
3217 if ( !errors[ i ].isRecoverable() ) {
3218 recoverable = false;
3219 }
3220 if ( errors[ i ].isWarning() ) {
3221 warning = true;
3222 }
3223 $item = $( '<div>' )
3224 .addClass( 'oo-ui-processDialog-error' )
3225 .append( errors[ i ].getMessage() );
3226 items.push( $item[ 0 ] );
3227 }
3228 this.$errorItems = $( items );
3229 if ( recoverable ) {
3230 abilities[ this.currentAction ] = true;
3231 // Copy the flags from the first matching action
3232 actions = this.actions.get( { actions: this.currentAction } );
3233 if ( actions.length ) {
3234 this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
3235 }
3236 } else {
3237 abilities[ this.currentAction ] = false;
3238 this.actions.setAbilities( abilities );
3239 }
3240 if ( warning ) {
3241 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
3242 } else {
3243 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
3244 }
3245 this.retryButton.toggle( recoverable );
3246 this.$errorsTitle.after( this.$errorItems );
3247 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
3248 };
3249
3250 /**
3251 * Hide errors.
3252 *
3253 * @private
3254 */
3255 OO.ui.ProcessDialog.prototype.hideErrors = function () {
3256 this.$errors.addClass( 'oo-ui-element-hidden' );
3257 if ( this.$errorItems ) {
3258 this.$errorItems.remove();
3259 this.$errorItems = null;
3260 }
3261 };
3262
3263 /**
3264 * @inheritdoc
3265 */
3266 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
3267 // Parent method
3268 return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
3269 .first( function () {
3270 // Make sure to hide errors
3271 this.hideErrors();
3272 this.fitOnOpen = false;
3273 }, this );
3274 };
3275
3276 /**
3277 * @class OO.ui
3278 */
3279
3280 /**
3281 * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
3282 * OO.ui.confirm.
3283 *
3284 * @private
3285 * @return {OO.ui.WindowManager}
3286 */
3287 OO.ui.getWindowManager = function () {
3288 if ( !OO.ui.windowManager ) {
3289 OO.ui.windowManager = new OO.ui.WindowManager();
3290 $( 'body' ).append( OO.ui.windowManager.$element );
3291 OO.ui.windowManager.addWindows( [ new OO.ui.MessageDialog() ] );
3292 }
3293 return OO.ui.windowManager;
3294 };
3295
3296 /**
3297 * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
3298 * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
3299 * has only one action button, labelled "OK", clicking it will simply close the dialog.
3300 *
3301 * A window manager is created automatically when this function is called for the first time.
3302 *
3303 * @example
3304 * OO.ui.alert( 'Something happened!' ).done( function () {
3305 * console.log( 'User closed the dialog.' );
3306 * } );
3307 *
3308 * OO.ui.alert( 'Something larger happened!', { size: 'large' } );
3309 *
3310 * @param {jQuery|string} text Message text to display
3311 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3312 * @return {jQuery.Promise} Promise resolved when the user closes the dialog
3313 */
3314 OO.ui.alert = function ( text, options ) {
3315 return OO.ui.getWindowManager().openWindow( 'message', $.extend( {
3316 message: text,
3317 actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
3318 }, options ) ).then( function ( opened ) {
3319 return opened.then( function ( closing ) {
3320 return closing.then( function () {
3321 return $.Deferred().resolve();
3322 } );
3323 } );
3324 } );
3325 };
3326
3327 /**
3328 * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
3329 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3330 * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
3331 * (labelled "Cancel").
3332 *
3333 * A window manager is created automatically when this function is called for the first time.
3334 *
3335 * @example
3336 * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
3337 * if ( confirmed ) {
3338 * console.log( 'User clicked "OK"!' );
3339 * } else {
3340 * console.log( 'User clicked "Cancel" or closed the dialog.' );
3341 * }
3342 * } );
3343 *
3344 * @param {jQuery|string} text Message text to display
3345 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3346 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3347 * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
3348 * `false`.
3349 */
3350 OO.ui.confirm = function ( text, options ) {
3351 return OO.ui.getWindowManager().openWindow( 'message', $.extend( {
3352 message: text
3353 }, options ) ).then( function ( opened ) {
3354 return opened.then( function ( closing ) {
3355 return closing.then( function ( data ) {
3356 return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
3357 } );
3358 } );
3359 } );
3360 };
3361
3362 /**
3363 * Display a quick modal prompt dialog, using a OO.ui.MessageDialog. While the dialog is open,
3364 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3365 * dialog has a text input widget and two action buttons, one to confirm an operation (labelled "OK")
3366 * and one to cancel it (labelled "Cancel").
3367 *
3368 * A window manager is created automatically when this function is called for the first time.
3369 *
3370 * @example
3371 * OO.ui.prompt( 'Choose a line to go to', { textInput: { placeholder: 'Line number' } } ).done( function ( result ) {
3372 * if ( result !== null ) {
3373 * console.log( 'User typed "' + result + '" then clicked "OK".' );
3374 * } else {
3375 * console.log( 'User clicked "Cancel" or closed the dialog.' );
3376 * }
3377 * } );
3378 *
3379 * @param {jQuery|string} text Message text to display
3380 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3381 * @param {Object} [options.textInput] Additional options for text input widget, see OO.ui.TextInputWidget
3382 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3383 * confirm, the promise will resolve with the value of the text input widget; otherwise, it will
3384 * resolve to `null`.
3385 */
3386 OO.ui.prompt = function ( text, options ) {
3387 var manager = OO.ui.getWindowManager(),
3388 textInput = new OO.ui.TextInputWidget( ( options && options.textInput ) || {} ),
3389 textField = new OO.ui.FieldLayout( textInput, {
3390 align: 'top',
3391 label: text
3392 } );
3393
3394 // TODO: This is a little hacky, and could be done by extending MessageDialog instead.
3395
3396 return manager.openWindow( 'message', $.extend( {
3397 message: textField.$element
3398 }, options ) ).then( function ( opened ) {
3399 // After ready
3400 textInput.on( 'enter', function () {
3401 manager.getCurrentWindow().close( { action: 'accept' } );
3402 } );
3403 textInput.focus();
3404 return opened.then( function ( closing ) {
3405 return closing.then( function ( data ) {
3406 return $.Deferred().resolve( data && data.action === 'accept' ? textInput.getValue() : null );
3407 } );
3408 } );
3409 } );
3410 };
3411
3412 }( OO ) );