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