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