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