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