3 * https://www.mediawiki.org/wiki/OOUI
5 * Copyright 2011–2019 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2019-07-10T12:25:07Z
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
20 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
21 * Please see the [OOUI documentation on MediaWiki] [1] for more information
24 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
27 * @extends OO.ui.ButtonWidget
28 * @mixins OO.ui.mixin.PendingElement
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
38 OO
.ui
.ActionWidget
= function OoUiActionWidget( config
) {
39 // Configuration initialization
40 config
= $.extend( { framed
: false }, config
);
43 OO
.ui
.ActionWidget
.parent
.call( this, config
);
46 OO
.ui
.mixin
.PendingElement
.call( this, config
);
49 this.action
= config
.action
|| '';
50 this.modes
= config
.modes
|| [];
55 this.$element
.addClass( 'oo-ui-actionWidget' );
60 OO
.inheritClass( OO
.ui
.ActionWidget
, OO
.ui
.ButtonWidget
);
61 OO
.mixinClass( OO
.ui
.ActionWidget
, OO
.ui
.mixin
.PendingElement
);
66 * Check if the action is configured to be available in the specified `mode`.
68 * @param {string} mode Name of mode
69 * @return {boolean} The action is configured with the mode
71 OO
.ui
.ActionWidget
.prototype.hasMode = function ( mode
) {
72 return this.modes
.indexOf( mode
) !== -1;
76 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
80 OO
.ui
.ActionWidget
.prototype.getAction = function () {
85 * Get the symbolic name of the mode or modes for which the action is configured to be available.
87 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
88 * Only actions that are configured to be available in the current mode will be visible.
89 * All other actions are hidden.
93 OO
.ui
.ActionWidget
.prototype.getModes = function () {
94 return this.modes
.slice();
97 /* eslint-disable no-unused-vars */
99 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that
101 * Actions can be made available for specific contexts (modes) and circumstances
102 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
104 * ActionSets contain two types of actions:
106 * - Special: Special actions are the first visible actions with special flags, such as 'safe' and
107 * 'primary', the default special flags. Additional special flags can be configured in subclasses
108 * with the static #specialFlags property.
109 * - Other: Other actions include all non-special visible actions.
111 * See the [OOUI documentation on MediaWiki][1] for more information.
114 * // Example: An action set used in a process dialog
115 * function MyProcessDialog( config ) {
116 * MyProcessDialog.parent.call( this, config );
118 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
119 * MyProcessDialog.static.title = 'An action set in a process dialog';
120 * MyProcessDialog.static.name = 'myProcessDialog';
121 * // An action set that uses modes ('edit' and 'help' mode, in this example).
122 * MyProcessDialog.static.actions = [
124 * action: 'continue',
127 * flags: [ 'primary', 'progressive' ]
129 * { action: 'help', modes: 'edit', label: 'Help' },
130 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
131 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
134 * MyProcessDialog.prototype.initialize = function () {
135 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
136 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
137 * this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, ' +
138 * 'cancel, back) configured with modes. This is edit mode. Click \'help\' to see ' +
139 * 'help mode.</p>' );
140 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
141 * this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget ' +
142 * 'is configured to be visible here. Click \'back\' to return to \'edit\' mode.' +
144 * this.stackLayout = new OO.ui.StackLayout( {
145 * items: [ this.panel1, this.panel2 ]
147 * this.$body.append( this.stackLayout.$element );
149 * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
150 * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
151 * .next( function () {
152 * this.actions.setMode( 'edit' );
155 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
156 * if ( action === 'help' ) {
157 * this.actions.setMode( 'help' );
158 * this.stackLayout.setItem( this.panel2 );
159 * } else if ( action === 'back' ) {
160 * this.actions.setMode( 'edit' );
161 * this.stackLayout.setItem( this.panel1 );
162 * } else if ( action === 'continue' ) {
164 * return new OO.ui.Process( function () {
168 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
170 * MyProcessDialog.prototype.getBodyHeight = function () {
171 * return this.panel1.$element.outerHeight( true );
173 * var windowManager = new OO.ui.WindowManager();
174 * $( document.body ).append( windowManager.$element );
175 * var dialog = new MyProcessDialog( {
178 * windowManager.addWindows( [ dialog ] );
179 * windowManager.openWindow( dialog );
181 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
185 * @mixins OO.EventEmitter
188 * @param {Object} [config] Configuration options
190 OO
.ui
.ActionSet
= function OoUiActionSet( config
) {
191 // Configuration initialization
192 config
= config
|| {};
194 // Mixin constructors
195 OO
.EventEmitter
.call( this );
200 actions
: 'getAction',
204 this.categorized
= {};
207 this.organized
= false;
208 this.changing
= false;
209 this.changed
= false;
211 /* eslint-enable no-unused-vars */
215 OO
.mixinClass( OO
.ui
.ActionSet
, OO
.EventEmitter
);
217 /* Static Properties */
220 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
221 * header of a {@link OO.ui.ProcessDialog process dialog}.
222 * See the [OOUI documentation on MediaWiki][2] for more information and examples.
224 * [2]:https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs
231 OO
.ui
.ActionSet
.static.specialFlags
= [ 'safe', 'primary' ];
238 * A 'click' event is emitted when an action is clicked.
240 * @param {OO.ui.ActionWidget} action Action that was clicked
246 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
248 * @param {OO.ui.ActionWidget[]} added Actions added
254 * A 'remove' event is emitted when actions are {@link #method-remove removed}
255 * or {@link #clear cleared}.
257 * @param {OO.ui.ActionWidget[]} added Actions removed
263 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
264 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode}
272 * Handle action change events.
277 OO
.ui
.ActionSet
.prototype.onActionChange = function () {
278 this.organized
= false;
279 if ( this.changing
) {
282 this.emit( 'change' );
287 * Check if an action is one of the special actions.
289 * @param {OO.ui.ActionWidget} action Action to check
290 * @return {boolean} Action is special
292 OO
.ui
.ActionSet
.prototype.isSpecial = function ( action
) {
295 for ( flag
in this.special
) {
296 if ( action
=== this.special
[ flag
] ) {
305 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
308 * @param {Object} [filters] Filters to use, omit to get all actions
309 * @param {string|string[]} [filters.actions] Actions that action widgets must have
310 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
311 * @param {string|string[]} [filters.modes] Modes that action widgets must have
312 * @param {boolean} [filters.visible] Action widgets must be visible
313 * @param {boolean} [filters.disabled] Action widgets must be disabled
314 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
316 OO
.ui
.ActionSet
.prototype.get = function ( filters
) {
317 var i
, len
, list
, category
, actions
, index
, match
, matches
;
322 // Collect category candidates
324 for ( category
in this.categorized
) {
325 list
= filters
[ category
];
327 if ( !Array
.isArray( list
) ) {
330 for ( i
= 0, len
= list
.length
; i
< len
; i
++ ) {
331 actions
= this.categorized
[ category
][ list
[ i
] ];
332 if ( Array
.isArray( actions
) ) {
333 matches
.push
.apply( matches
, actions
);
338 // Remove by boolean filters
339 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
340 match
= matches
[ i
];
342 ( filters
.visible
!== undefined && match
.isVisible() !== filters
.visible
) ||
343 ( filters
.disabled
!== undefined && match
.isDisabled() !== filters
.disabled
)
345 matches
.splice( i
, 1 );
351 for ( i
= 0, len
= matches
.length
; i
< len
; i
++ ) {
352 match
= matches
[ i
];
353 index
= matches
.lastIndexOf( match
);
354 while ( index
!== i
) {
355 matches
.splice( index
, 1 );
357 index
= matches
.lastIndexOf( match
);
362 return this.list
.slice();
366 * Get 'special' actions.
368 * Special actions are the first visible action widgets with special flags, such as 'safe' and
370 * Special flags can be configured in subclasses by changing the static #specialFlags property.
372 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
374 OO
.ui
.ActionSet
.prototype.getSpecial = function () {
376 return $.extend( {}, this.special
);
380 * Get 'other' actions.
382 * Other actions include all non-special visible action widgets.
384 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
386 OO
.ui
.ActionSet
.prototype.getOthers = function () {
388 return this.others
.slice();
392 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
393 * to be available in the specified mode will be made visible. All other actions will be hidden.
395 * @param {string} mode The mode. Only actions configured to be available in the specified
396 * mode will be made visible.
398 * @return {OO.ui.ActionSet} The widget, for chaining
402 OO
.ui
.ActionSet
.prototype.setMode = function ( mode
) {
405 this.changing
= true;
406 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
407 action
= this.list
[ i
];
408 action
.toggle( action
.hasMode( mode
) );
411 this.organized
= false;
412 this.changing
= false;
413 this.emit( 'change' );
419 * Set the abilities of the specified actions.
421 * Action widgets that are configured with the specified actions will be enabled
422 * or disabled based on the boolean values specified in the `actions`
425 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
426 * values that indicate whether or not the action should be enabled.
428 * @return {OO.ui.ActionSet} The widget, for chaining
430 OO
.ui
.ActionSet
.prototype.setAbilities = function ( actions
) {
431 var i
, len
, action
, item
;
433 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
434 item
= this.list
[ i
];
435 action
= item
.getAction();
436 if ( actions
[ action
] !== undefined ) {
437 item
.setDisabled( !actions
[ action
] );
445 * Executes a function once per action.
447 * When making changes to multiple actions, use this method instead of iterating over the actions
448 * manually to defer emitting a #change event until after all actions have been changed.
450 * @param {Object|null} filter Filters to use to determine which actions to iterate over; see #get
451 * @param {Function} callback Callback to run for each action; callback is invoked with three
452 * arguments: the action, the action's index, the list of actions being iterated over
454 * @return {OO.ui.ActionSet} The widget, for chaining
456 OO
.ui
.ActionSet
.prototype.forEach = function ( filter
, callback
) {
457 this.changed
= false;
458 this.changing
= true;
459 this.get( filter
).forEach( callback
);
460 this.changing
= false;
461 if ( this.changed
) {
462 this.emit( 'change' );
469 * Add action widgets to the action set.
471 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
473 * @return {OO.ui.ActionSet} The widget, for chaining
477 OO
.ui
.ActionSet
.prototype.add = function ( actions
) {
480 this.changing
= true;
481 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
482 action
= actions
[ i
];
483 action
.connect( this, {
484 click
: [ 'emit', 'click', action
],
485 toggle
: [ 'onActionChange' ]
487 this.list
.push( action
);
489 this.organized
= false;
490 this.emit( 'add', actions
);
491 this.changing
= false;
492 this.emit( 'change' );
498 * Remove action widgets from the set.
500 * To remove all actions, you may wish to use the #clear method instead.
502 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
504 * @return {OO.ui.ActionSet} The widget, for chaining
508 OO
.ui
.ActionSet
.prototype.remove = function ( actions
) {
509 var i
, len
, index
, action
;
511 this.changing
= true;
512 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
513 action
= actions
[ i
];
514 index
= this.list
.indexOf( action
);
515 if ( index
!== -1 ) {
516 action
.disconnect( this );
517 this.list
.splice( index
, 1 );
520 this.organized
= false;
521 this.emit( 'remove', actions
);
522 this.changing
= false;
523 this.emit( 'change' );
529 * Remove all action widgets from the set.
531 * To remove only specified actions, use the {@link #method-remove remove} method instead.
534 * @return {OO.ui.ActionSet} The widget, for chaining
538 OO
.ui
.ActionSet
.prototype.clear = function () {
540 removed
= this.list
.slice();
542 this.changing
= true;
543 for ( i
= 0, len
= this.list
.length
; i
< len
; i
++ ) {
544 action
= this.list
[ i
];
545 action
.disconnect( this );
550 this.organized
= false;
551 this.emit( 'remove', removed
);
552 this.changing
= false;
553 this.emit( 'change' );
561 * This is called whenever organized information is requested. It will only reorganize the actions
562 * if something has changed since the last time it ran.
566 * @return {OO.ui.ActionSet} The widget, for chaining
568 OO
.ui
.ActionSet
.prototype.organize = function () {
569 var i
, iLen
, j
, jLen
, flag
, action
, category
, list
, item
, special
,
570 specialFlags
= this.constructor.static.specialFlags
;
572 if ( !this.organized
) {
573 this.categorized
= {};
576 for ( i
= 0, iLen
= this.list
.length
; i
< iLen
; i
++ ) {
577 action
= this.list
[ i
];
578 if ( action
.isVisible() ) {
579 // Populate categories
580 for ( category
in this.categories
) {
581 if ( !this.categorized
[ category
] ) {
582 this.categorized
[ category
] = {};
584 list
= action
[ this.categories
[ category
] ]();
585 if ( !Array
.isArray( list
) ) {
588 for ( j
= 0, jLen
= list
.length
; j
< jLen
; j
++ ) {
590 if ( !this.categorized
[ category
][ item
] ) {
591 this.categorized
[ category
][ item
] = [];
593 this.categorized
[ category
][ item
].push( action
);
596 // Populate special/others
598 for ( j
= 0, jLen
= specialFlags
.length
; j
< jLen
; j
++ ) {
599 flag
= specialFlags
[ j
];
600 if ( !this.special
[ flag
] && action
.hasFlag( flag
) ) {
601 this.special
[ flag
] = action
;
607 this.others
.push( action
);
611 this.organized
= true;
618 * Errors contain a required message (either a string or jQuery selection) that is used to describe
619 * what went wrong in a {@link OO.ui.Process process}. The error's #recoverable and #warning
620 * configurations are used to customize the appearance and functionality of the error interface.
622 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss'
623 * and 'Try again' (i.e., the error is 'recoverable' by default). If the error is not recoverable,
624 * the 'Try again' button will not be rendered and the widget that initiated the failed process will
627 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button,
628 * which will try the process again.
630 * For an example of error interfaces, please see the [OOUI documentation on MediaWiki][1].
632 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Processes_and_errors
637 * @param {string|jQuery} message Description of error
638 * @param {Object} [config] Configuration options
639 * @cfg {boolean} [recoverable=true] Error is recoverable.
640 * By default, errors are recoverable, and users can try the process again.
641 * @cfg {boolean} [warning=false] Error is a warning.
642 * If the error is a warning, the error interface will include a
643 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the
644 * warning is not triggered a second time if the user chooses to continue.
646 OO
.ui
.Error
= function OoUiError( message
, config
) {
647 // Allow passing positional parameters inside the config object
648 if ( OO
.isPlainObject( message
) && config
=== undefined ) {
650 message
= config
.message
;
653 // Configuration initialization
654 config
= config
|| {};
657 this.message
= message
instanceof $ ? message
: String( message
);
658 this.recoverable
= config
.recoverable
=== undefined || !!config
.recoverable
;
659 this.warning
= !!config
.warning
;
664 OO
.initClass( OO
.ui
.Error
);
669 * Check if the error is recoverable.
671 * If the error is recoverable, users are able to try the process again.
673 * @return {boolean} Error is recoverable
675 OO
.ui
.Error
.prototype.isRecoverable = function () {
676 return this.recoverable
;
680 * Check if the error is a warning.
682 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
684 * @return {boolean} Error is warning
686 OO
.ui
.Error
.prototype.isWarning = function () {
691 * Get error message as DOM nodes.
693 * @return {jQuery} Error message in DOM nodes
695 OO
.ui
.Error
.prototype.getMessage = function () {
696 return this.message
instanceof $ ?
697 this.message
.clone() :
698 $( '<div>' ).text( this.message
).contents();
702 * Get the error message text.
704 * @return {string} Error message
706 OO
.ui
.Error
.prototype.getMessageText = function () {
707 return this.message
instanceof $ ? this.message
.text() : this.message
;
711 * A Process is a list of steps that are called in sequence. The step can be a number, a
712 * jQuery promise, or a function:
714 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
715 * - **promise**: the process will continue to the next step when the promise is successfully
716 * resolved or stop if the promise is rejected.
717 * - **function**: the process will execute the function. The process will stop if the function
718 * returns either a boolean `false` or a promise that is rejected; if the function returns a
719 * number, the process will wait for that number of milliseconds before proceeding.
721 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
722 * configured, users can dismiss the error and try the process again, or not. If a process is
723 * stopped, its remaining steps will not be performed.
728 * @param {number|jQuery.Promise|Function} step Number of milliseconds to wait before proceeding,
729 * promise that must be resolved before proceeding, or a function to execute. See #createStep for
730 * more information. See #createStep for more information.
731 * @param {Object} [context=null] Execution context of the function. The context is ignored if the
732 * step is a number or promise.
734 OO
.ui
.Process = function ( step
, context
) {
739 if ( step
!== undefined ) {
740 this.next( step
, context
);
746 OO
.initClass( OO
.ui
.Process
);
753 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
754 * If any of the steps return a promise that is rejected or a boolean false, this promise is
755 * rejected and any remaining steps are not performed.
757 OO
.ui
.Process
.prototype.execute = function () {
761 * Continue execution.
764 * @param {Array} step A function and the context it should be called in
765 * @return {Function} Function that continues the process
767 function proceed( step
) {
769 // Execute step in the correct context
771 result
= step
.callback
.call( step
.context
);
773 if ( result
=== false ) {
774 // Use rejected promise for boolean false results
775 return $.Deferred().reject( [] ).promise();
777 if ( typeof result
=== 'number' ) {
779 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
781 // Use a delayed promise for numbers, expecting them to be in milliseconds
782 deferred
= $.Deferred();
783 setTimeout( deferred
.resolve
, result
);
784 return deferred
.promise();
786 if ( result
instanceof OO
.ui
.Error
) {
787 // Use rejected promise for error
788 return $.Deferred().reject( [ result
] ).promise();
790 if ( Array
.isArray( result
) && result
.length
&& result
[ 0 ] instanceof OO
.ui
.Error
) {
791 // Use rejected promise for list of errors
792 return $.Deferred().reject( result
).promise();
794 // Duck-type the object to see if it can produce a promise
795 if ( result
&& typeof result
.promise
=== 'function' ) {
796 // Use a promise generated from the result
797 return result
.promise();
799 // Use resolved promise for other results
800 return $.Deferred().resolve().promise();
804 if ( this.steps
.length
) {
805 // Generate a chain reaction of promises
806 promise
= proceed( this.steps
[ 0 ] )();
807 for ( i
= 1, len
= this.steps
.length
; i
< len
; i
++ ) {
808 promise
= promise
.then( proceed( this.steps
[ i
] ) );
811 promise
= $.Deferred().resolve().promise();
818 * Create a process step.
821 * @param {number|jQuery.Promise|Function} step
823 * - Number of milliseconds to wait before proceeding
824 * - Promise that must be resolved before proceeding
825 * - Function to execute
826 * - If the function returns a boolean false the process will stop
827 * - If the function returns a promise, the process will continue to the next
828 * step when the promise is resolved or stop if the promise is rejected
829 * - If the function returns a number, the process will wait for that number of
830 * milliseconds before proceeding
831 * @param {Object} [context=null] Execution context of the function. The context is
832 * ignored if the step is a number or promise.
833 * @return {Object} Step object, with `callback` and `context` properties
835 OO
.ui
.Process
.prototype.createStep = function ( step
, context
) {
836 if ( typeof step
=== 'number' || typeof step
.promise
=== 'function' ) {
838 callback: function () {
844 if ( typeof step
=== 'function' ) {
850 throw new Error( 'Cannot create process step: number, promise or function expected' );
854 * Add step to the beginning of the process.
856 * @inheritdoc #createStep
857 * @return {OO.ui.Process} this
860 OO
.ui
.Process
.prototype.first = function ( step
, context
) {
861 this.steps
.unshift( this.createStep( step
, context
) );
866 * Add step to the end of the process.
868 * @inheritdoc #createStep
869 * @return {OO.ui.Process} this
872 OO
.ui
.Process
.prototype.next = function ( step
, context
) {
873 this.steps
.push( this.createStep( step
, context
) );
878 * A window instance represents the life cycle for one single opening of a window
881 * While OO.ui.WindowManager will reuse OO.ui.Window objects, each time a window is
882 * opened, a new lifecycle starts.
884 * For more information, please see the [OOUI documentation on MediaWiki] [1].
886 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows
892 OO
.ui
.WindowInstance
= function OoUiWindowInstance() {
894 opening
: $.Deferred(),
895 opened
: $.Deferred(),
896 closing
: $.Deferred(),
904 this.deferreds
= deferreds
;
906 // Set these up as chained promises so that rejecting of
907 // an earlier stage automatically rejects the subsequent
908 // would-be stages as well.
911 * @property {jQuery.Promise}
913 this.opening
= deferreds
.opening
.promise();
915 * @property {jQuery.Promise}
917 this.opened
= this.opening
.then( function () {
918 return deferreds
.opened
;
921 * @property {jQuery.Promise}
923 this.closing
= this.opened
.then( function () {
924 return deferreds
.closing
;
927 * @property {jQuery.Promise}
929 this.closed
= this.closing
.then( function () {
930 return deferreds
.closed
;
936 OO
.initClass( OO
.ui
.WindowInstance
);
939 * Check if window is opening.
941 * @return {boolean} Window is opening
943 OO
.ui
.WindowInstance
.prototype.isOpening = function () {
944 return this.deferreds
.opened
.state() === 'pending';
948 * Check if window is opened.
950 * @return {boolean} Window is opened
952 OO
.ui
.WindowInstance
.prototype.isOpened = function () {
953 return this.deferreds
.opened
.state() === 'resolved' &&
954 this.deferreds
.closing
.state() === 'pending';
958 * Check if window is closing.
960 * @return {boolean} Window is closing
962 OO
.ui
.WindowInstance
.prototype.isClosing = function () {
963 return this.deferreds
.closing
.state() === 'resolved' &&
964 this.deferreds
.closed
.state() === 'pending';
968 * Check if window is closed.
970 * @return {boolean} Window is closed
972 OO
.ui
.WindowInstance
.prototype.isClosed = function () {
973 return this.deferreds
.closed
.state() === 'resolved';
977 * Window managers are used to open and close {@link OO.ui.Window windows} and control their
978 * presentation. Managed windows are mutually exclusive. If a new window is opened while a current
979 * window is opening or is opened, the current window will be closed and any on-going
980 * {@link OO.ui.Process process} will be cancelled. Windows
981 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
982 * pertinent data and reused.
984 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
985 * `opened`, and `closing`, which represent the primary stages of the cycle:
987 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
988 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
990 * - an `opening` event is emitted with an `opening` promise
991 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution
992 * before the window’s {@link OO.ui.Window#method-setup setup} method is called which executes
993 * OO.ui.Window#getSetupProcess.
994 * - a `setup` progress notification is emitted from the `opening` promise
995 * - the #getReadyDelay method is called the returned value is used to time a pause in execution
996 * before the window’s {@link OO.ui.Window#method-ready ready} method is called which executes
997 * OO.ui.Window#getReadyProcess.
998 * - a `ready` progress notification is emitted from the `opening` promise
999 * - the `opening` promise is resolved with an `opened` promise
1001 * **Opened**: the window is now open.
1003 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
1004 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
1005 * to close the window.
1007 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
1008 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution
1009 * before the window's {@link OO.ui.Window#getHoldProcess getHoldProcess} method is called on the
1010 * window and its result executed
1011 * - a `hold` progress notification is emitted from the `closing` promise
1012 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in
1013 * execution before the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method
1014 * is called on the window and its result executed
1015 * - a `teardown` progress notification is emitted from the `closing` promise
1016 * - the `closing` promise is resolved. The window is now closed
1018 * See the [OOUI documentation on MediaWiki][1] for more information.
1020 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
1023 * @extends OO.ui.Element
1024 * @mixins OO.EventEmitter
1027 * @param {Object} [config] Configuration options
1028 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
1029 * Note that window classes that are instantiated with a factory must have
1030 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
1031 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
1033 OO
.ui
.WindowManager
= function OoUiWindowManager( config
) {
1034 // Configuration initialization
1035 config
= config
|| {};
1037 // Parent constructor
1038 OO
.ui
.WindowManager
.parent
.call( this, config
);
1040 // Mixin constructors
1041 OO
.EventEmitter
.call( this );
1044 this.factory
= config
.factory
;
1045 this.modal
= config
.modal
=== undefined || !!config
.modal
;
1047 // Deprecated placeholder promise given to compatOpening in openWindow()
1048 // that is resolved in closeWindow().
1049 this.compatOpened
= null;
1050 this.preparingToOpen
= null;
1051 this.preparingToClose
= null;
1052 this.currentWindow
= null;
1053 this.globalEvents
= false;
1054 this.$returnFocusTo
= null;
1055 this.$ariaHidden
= null;
1056 this.onWindowResizeTimeout
= null;
1057 this.onWindowResizeHandler
= this.onWindowResize
.bind( this );
1058 this.afterWindowResizeHandler
= this.afterWindowResize
.bind( this );
1062 .addClass( 'oo-ui-windowManager' )
1063 .toggleClass( 'oo-ui-windowManager-modal', this.modal
);
1065 this.$element
.attr( 'aria-hidden', true );
1071 OO
.inheritClass( OO
.ui
.WindowManager
, OO
.ui
.Element
);
1072 OO
.mixinClass( OO
.ui
.WindowManager
, OO
.EventEmitter
);
1077 * An 'opening' event is emitted when the window begins to be opened.
1080 * @param {OO.ui.Window} win Window that's being opened
1081 * @param {jQuery.Promise} opened A promise resolved with a value when the window is opened
1082 * successfully. This promise also emits `setup` and `ready` notifications. When this promise is
1083 * resolved, the first argument of the value is an 'closed' promise, the second argument is the
1085 * @param {Object} data Window opening data
1089 * A 'closing' event is emitted when the window begins to be closed.
1092 * @param {OO.ui.Window} win Window that's being closed
1093 * @param {jQuery.Promise} closed A promise resolved with a value when the window is closed
1094 * successfully. This promise also emits `hold` and `teardown` notifications. When this promise is
1095 * resolved, the first argument of its value is the closing data.
1096 * @param {Object} data Window closing data
1100 * A 'resize' event is emitted when a window is resized.
1103 * @param {OO.ui.Window} win Window that was resized
1106 /* Static Properties */
1109 * Map of the symbolic name of each window size and its CSS properties.
1113 * @property {Object}
1115 OO
.ui
.WindowManager
.static.sizes
= {
1129 // These can be non-numeric because they are never used in calculations
1136 * Symbolic name of the default window size.
1138 * The default size is used if the window's requested size is not recognized.
1142 * @property {string}
1144 OO
.ui
.WindowManager
.static.defaultSize
= 'medium';
1149 * Handle window resize events.
1152 * @param {jQuery.Event} e Window resize event
1154 OO
.ui
.WindowManager
.prototype.onWindowResize = function () {
1155 clearTimeout( this.onWindowResizeTimeout
);
1156 this.onWindowResizeTimeout
= setTimeout( this.afterWindowResizeHandler
, 200 );
1160 * Handle window resize events.
1163 * @param {jQuery.Event} e Window resize event
1165 OO
.ui
.WindowManager
.prototype.afterWindowResize = function () {
1166 var currentFocusedElement
= document
.activeElement
;
1167 if ( this.currentWindow
) {
1168 this.updateWindowSize( this.currentWindow
);
1170 // Restore focus to the original element if it has changed.
1171 // When a layout change is made on resize inputs lose focus
1172 // on Android (Chrome and Firefox), see T162127.
1173 if ( currentFocusedElement
!== document
.activeElement
) {
1174 currentFocusedElement
.focus();
1180 * Check if window is opening.
1182 * @param {OO.ui.Window} win Window to check
1183 * @return {boolean} Window is opening
1185 OO
.ui
.WindowManager
.prototype.isOpening = function ( win
) {
1186 return win
=== this.currentWindow
&& !!this.lifecycle
&&
1187 this.lifecycle
.isOpening();
1191 * Check if window is closing.
1193 * @param {OO.ui.Window} win Window to check
1194 * @return {boolean} Window is closing
1196 OO
.ui
.WindowManager
.prototype.isClosing = function ( win
) {
1197 return win
=== this.currentWindow
&& !!this.lifecycle
&&
1198 this.lifecycle
.isClosing();
1202 * Check if window is opened.
1204 * @param {OO.ui.Window} win Window to check
1205 * @return {boolean} Window is opened
1207 OO
.ui
.WindowManager
.prototype.isOpened = function ( win
) {
1208 return win
=== this.currentWindow
&& !!this.lifecycle
&&
1209 this.lifecycle
.isOpened();
1213 * Check if a window is being managed.
1215 * @param {OO.ui.Window} win Window to check
1216 * @return {boolean} Window is being managed
1218 OO
.ui
.WindowManager
.prototype.hasWindow = function ( win
) {
1221 for ( name
in this.windows
) {
1222 if ( this.windows
[ name
] === win
) {
1231 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
1233 * @param {OO.ui.Window} win Window being opened
1234 * @param {Object} [data] Window opening data
1235 * @return {number} Milliseconds to wait
1237 OO
.ui
.WindowManager
.prototype.getSetupDelay = function () {
1242 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’
1245 * @param {OO.ui.Window} win Window being opened
1246 * @param {Object} [data] Window opening data
1247 * @return {number} Milliseconds to wait
1249 OO
.ui
.WindowManager
.prototype.getReadyDelay = function () {
1250 return this.modal
? OO
.ui
.theme
.getDialogTransitionDuration() : 0;
1254 * Get the number of milliseconds to wait after closing has begun before executing the 'hold'
1257 * @param {OO.ui.Window} win Window being closed
1258 * @param {Object} [data] Window closing data
1259 * @return {number} Milliseconds to wait
1261 OO
.ui
.WindowManager
.prototype.getHoldDelay = function () {
1266 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
1267 * executing the ‘teardown’ process.
1269 * @param {OO.ui.Window} win Window being closed
1270 * @param {Object} [data] Window closing data
1271 * @return {number} Milliseconds to wait
1273 OO
.ui
.WindowManager
.prototype.getTeardownDelay = function () {
1274 return this.modal
? OO
.ui
.theme
.getDialogTransitionDuration() : 0;
1278 * Get a window by its symbolic name.
1280 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will
1281 * be instantiated and added to the window manager automatically. Please see the [OOUI documentation
1282 * on MediaWiki][3] for more information about using factories.
1283 * [3]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
1285 * @param {string} name Symbolic name of the window
1286 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
1287 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
1288 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
1290 OO
.ui
.WindowManager
.prototype.getWindow = function ( name
) {
1291 var deferred
= $.Deferred(),
1292 win
= this.windows
[ name
];
1294 if ( !( win
instanceof OO
.ui
.Window
) ) {
1295 if ( this.factory
) {
1296 if ( !this.factory
.lookup( name
) ) {
1297 deferred
.reject( new OO
.ui
.Error(
1298 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
1301 win
= this.factory
.create( name
);
1302 this.addWindows( [ win
] );
1303 deferred
.resolve( win
);
1306 deferred
.reject( new OO
.ui
.Error(
1307 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
1311 deferred
.resolve( win
);
1314 return deferred
.promise();
1318 * Get current window.
1320 * @return {OO.ui.Window|null} Currently opening/opened/closing window
1322 OO
.ui
.WindowManager
.prototype.getCurrentWindow = function () {
1323 return this.currentWindow
;
1326 /* eslint-disable valid-jsdoc */
1330 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
1331 * @param {Object} [data] Window opening data
1332 * @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when
1333 * closed. Defaults the current activeElement. If set to null, focus isn't changed on close.
1334 * @return {OO.ui.WindowInstance} A lifecycle object representing this particular
1335 * opening of the window. For backwards-compatibility, then object is also a Thenable that is
1336 * resolved when the window is done opening, with nested promise for when closing starts. This
1337 * behaviour is deprecated and is not compatible with jQuery 3, see T163510.
1340 OO
.ui
.WindowManager
.prototype.openWindow = function ( win
, data
, lifecycle
, compatOpening
) {
1341 /* eslint-enable valid-jsdoc */
1346 // Internal parameter 'lifecycle' allows this method to always return
1347 // a lifecycle even if the window still needs to be created
1348 // asynchronously when 'win' is a string.
1349 lifecycle
= lifecycle
|| new OO
.ui
.WindowInstance();
1350 compatOpening
= compatOpening
|| $.Deferred();
1352 // Turn lifecycle into a Thenable for backwards-compatibility with
1353 // the deprecated nested-promise behaviour, see T163510.
1354 [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
1355 .forEach( function ( method
) {
1356 lifecycle
[ method
] = function () {
1357 OO
.ui
.warnDeprecation(
1358 'Using the return value of openWindow as a promise is deprecated. ' +
1359 'Use .openWindow( ... ).opening.' + method
+ '( ... ) instead.'
1361 return compatOpening
[ method
].apply( this, arguments
);
1365 // Argument handling
1366 if ( typeof win
=== 'string' ) {
1367 this.getWindow( win
).then(
1369 manager
.openWindow( win
, data
, lifecycle
, compatOpening
);
1372 lifecycle
.deferreds
.opening
.reject( err
);
1379 if ( !this.hasWindow( win
) ) {
1380 error
= 'Cannot open window: window is not attached to manager';
1381 } else if ( this.lifecycle
&& this.lifecycle
.isOpened() ) {
1382 error
= 'Cannot open window: another window is open';
1383 } else if ( this.preparingToOpen
|| ( this.lifecycle
&& this.lifecycle
.isOpening() ) ) {
1384 error
= 'Cannot open window: another window is opening';
1388 compatOpening
.reject( new OO
.ui
.Error( error
) );
1389 lifecycle
.deferreds
.opening
.reject( new OO
.ui
.Error( error
) );
1393 // If a window is currently closing, wait for it to complete
1394 this.preparingToOpen
= $.when( this.lifecycle
&& this.lifecycle
.closed
);
1395 // Ensure handlers get called after preparingToOpen is set
1396 this.preparingToOpen
.done( function () {
1397 if ( manager
.modal
) {
1398 manager
.toggleGlobalEvents( true );
1399 manager
.toggleAriaIsolation( true );
1401 manager
.$returnFocusTo
= data
.$returnFocusTo
!== undefined ?
1402 data
.$returnFocusTo
:
1403 $( document
.activeElement
);
1404 manager
.currentWindow
= win
;
1405 manager
.lifecycle
= lifecycle
;
1406 manager
.preparingToOpen
= null;
1407 manager
.emit( 'opening', win
, compatOpening
, data
);
1408 lifecycle
.deferreds
.opening
.resolve( data
);
1409 setTimeout( function () {
1410 manager
.compatOpened
= $.Deferred();
1411 win
.setup( data
).then( function () {
1412 compatOpening
.notify( { state
: 'setup' } );
1413 setTimeout( function () {
1414 win
.ready( data
).then( function () {
1415 compatOpening
.notify( { state
: 'ready' } );
1416 lifecycle
.deferreds
.opened
.resolve( data
);
1417 compatOpening
.resolve( manager
.compatOpened
.promise(), data
);
1419 lifecycle
.deferreds
.opened
.reject();
1420 compatOpening
.reject();
1421 manager
.closeWindow( win
);
1423 }, manager
.getReadyDelay() );
1425 lifecycle
.deferreds
.opened
.reject();
1426 compatOpening
.reject();
1427 manager
.closeWindow( win
);
1429 }, manager
.getSetupDelay() );
1438 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
1439 * @param {Object} [data] Window closing data
1440 * @return {OO.ui.WindowInstance} A lifecycle object representing this particular
1441 * opening of the window. For backwards-compatibility, the object is also a Thenable that is
1442 * resolved when the window is done closing, see T163510.
1445 OO
.ui
.WindowManager
.prototype.closeWindow = function ( win
, data
) {
1448 compatClosing
= $.Deferred(),
1449 lifecycle
= this.lifecycle
,
1452 // Argument handling
1453 if ( typeof win
=== 'string' ) {
1454 win
= this.windows
[ win
];
1455 } else if ( !this.hasWindow( win
) ) {
1461 error
= 'Cannot close window: no window is currently open';
1462 } else if ( !win
) {
1463 error
= 'Cannot close window: window is not attached to manager';
1464 } else if ( win
!== this.currentWindow
|| this.lifecycle
.isClosed() ) {
1465 error
= 'Cannot close window: window already closed with different data';
1466 } else if ( this.preparingToClose
|| this.lifecycle
.isClosing() ) {
1467 error
= 'Cannot close window: window already closing with different data';
1471 // This function was called for the wrong window and we don't want to mess with the current
1473 lifecycle
= new OO
.ui
.WindowInstance();
1474 // Pretend the window has been opened, so that we can pretend to fail to close it.
1475 lifecycle
.deferreds
.opening
.resolve( {} );
1476 lifecycle
.deferreds
.opened
.resolve( {} );
1479 // Turn lifecycle into a Thenable for backwards-compatibility with
1480 // the deprecated nested-promise behaviour, see T163510.
1481 [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
1482 .forEach( function ( method
) {
1483 lifecycle
[ method
] = function () {
1484 OO
.ui
.warnDeprecation(
1485 'Using the return value of closeWindow as a promise is deprecated. ' +
1486 'Use .closeWindow( ... ).closed.' + method
+ '( ... ) instead.'
1488 return compatClosing
[ method
].apply( this, arguments
);
1493 compatClosing
.reject( new OO
.ui
.Error( error
) );
1494 lifecycle
.deferreds
.closing
.reject( new OO
.ui
.Error( error
) );
1498 // If the window is currently opening, close it when it's done
1499 this.preparingToClose
= $.when( this.lifecycle
.opened
);
1500 // Ensure handlers get called after preparingToClose is set
1501 this.preparingToClose
.always( function () {
1502 manager
.preparingToClose
= null;
1503 manager
.emit( 'closing', win
, compatClosing
, data
);
1504 lifecycle
.deferreds
.closing
.resolve( data
);
1505 compatOpened
= manager
.compatOpened
;
1506 manager
.compatOpened
= null;
1507 compatOpened
.resolve( compatClosing
.promise(), data
);
1508 setTimeout( function () {
1509 win
.hold( data
).then( function () {
1510 compatClosing
.notify( { state
: 'hold' } );
1511 setTimeout( function () {
1512 win
.teardown( data
).then( function () {
1513 compatClosing
.notify( { state
: 'teardown' } );
1514 if ( manager
.modal
) {
1515 manager
.toggleGlobalEvents( false );
1516 manager
.toggleAriaIsolation( false );
1518 if ( manager
.$returnFocusTo
&& manager
.$returnFocusTo
.length
) {
1519 manager
.$returnFocusTo
[ 0 ].focus();
1521 manager
.currentWindow
= null;
1522 manager
.lifecycle
= null;
1523 lifecycle
.deferreds
.closed
.resolve( data
);
1524 compatClosing
.resolve( data
);
1526 }, manager
.getTeardownDelay() );
1528 }, manager
.getHoldDelay() );
1535 * Add windows to the window manager.
1537 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
1538 * See the [OOUI documentation on MediaWiki] [2] for examples.
1539 * [2]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
1541 * This function can be called in two manners:
1543 * 1. `.addWindows( [ winA, winB, ... ] )` (where `winA`, `winB` are OO.ui.Window objects)
1545 * This syntax registers windows under the symbolic names defined in their `.static.name`
1546 * properties. For example, if `windowA.constructor.static.name` is `'nameA'`, calling
1547 * `.openWindow( 'nameA' )` afterwards will open the window `windowA`. This syntax requires the
1548 * static name to be set, otherwise an exception will be thrown.
1550 * This is the recommended way, as it allows for an easier switch to using a window factory.
1552 * 2. `.addWindows( { nameA: winA, nameB: winB, ... } )`
1554 * This syntax registers windows under the explicitly given symbolic names. In this example,
1555 * calling `.openWindow( 'nameA' )` afterwards will open the window `windowA`, regardless of what
1556 * its `.static.name` is set to. The static name is not required to be set.
1558 * This should only be used if you need to override the default symbolic names.
1562 * var windowManager = new OO.ui.WindowManager();
1563 * $( document.body ).append( windowManager.$element );
1565 * // Add a window under the default name: see OO.ui.MessageDialog.static.name
1566 * windowManager.addWindows( [ new OO.ui.MessageDialog() ] );
1567 * // Add a window under an explicit name
1568 * windowManager.addWindows( { myMessageDialog: new OO.ui.MessageDialog() } );
1570 * // Open window by default name
1571 * windowManager.openWindow( 'message' );
1572 * // Open window by explicitly given name
1573 * windowManager.openWindow( 'myMessageDialog' );
1576 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
1577 * by reference, symbolic name, or explicitly defined symbolic names.
1578 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
1579 * explicit nor a statically configured symbolic name.
1581 OO
.ui
.WindowManager
.prototype.addWindows = function ( windows
) {
1582 var i
, len
, win
, name
, list
;
1584 if ( Array
.isArray( windows
) ) {
1585 // Convert to map of windows by looking up symbolic names from static configuration
1587 for ( i
= 0, len
= windows
.length
; i
< len
; i
++ ) {
1588 name
= windows
[ i
].constructor.static.name
;
1590 throw new Error( 'Windows must have a `name` static property defined.' );
1592 list
[ name
] = windows
[ i
];
1594 } else if ( OO
.isPlainObject( windows
) ) {
1599 for ( name
in list
) {
1601 this.windows
[ name
] = win
.toggle( false );
1602 this.$element
.append( win
.$element
);
1603 win
.setManager( this );
1608 * Remove the specified windows from the windows manager.
1610 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish
1611 * to use the #clearWindows method instead. If you no longer need the window manager and want to
1612 * ensure that it no longer listens to events, use the #destroy method.
1614 * @param {string[]} names Symbolic names of windows to remove
1615 * @return {jQuery.Promise} Promise resolved when window is closed and removed
1616 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
1618 OO
.ui
.WindowManager
.prototype.removeWindows = function ( names
) {
1619 var i
, len
, win
, name
, cleanupWindow
,
1622 cleanup = function ( name
, win
) {
1623 delete manager
.windows
[ name
];
1624 win
.$element
.detach();
1627 for ( i
= 0, len
= names
.length
; i
< len
; i
++ ) {
1629 win
= this.windows
[ name
];
1631 throw new Error( 'Cannot remove window' );
1633 cleanupWindow
= cleanup
.bind( null, name
, win
);
1634 promises
.push( this.closeWindow( name
).closed
.then( cleanupWindow
, cleanupWindow
) );
1637 return $.when
.apply( $, promises
);
1641 * Remove all windows from the window manager.
1643 * Windows will be closed before they are removed. Note that the window manager, though not in use,
1644 * will still listen to events. If the window manager will not be used again, you may wish to use
1645 * the #destroy method instead. To remove just a subset of windows, use the #removeWindows method.
1647 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
1649 OO
.ui
.WindowManager
.prototype.clearWindows = function () {
1650 return this.removeWindows( Object
.keys( this.windows
) );
1654 * Set dialog size. In general, this method should not be called directly.
1656 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
1658 * @param {OO.ui.Window} win Window to update, should be the current window
1660 * @return {OO.ui.WindowManager} The manager, for chaining
1662 OO
.ui
.WindowManager
.prototype.updateWindowSize = function ( win
) {
1665 // Bypass for non-current, and thus invisible, windows
1666 if ( win
!== this.currentWindow
) {
1670 isFullscreen
= win
.getSize() === 'full';
1672 this.$element
.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen
);
1673 this.$element
.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen
);
1674 win
.setDimensions( win
.getSizeProperties() );
1676 this.emit( 'resize', win
);
1682 * Bind or unbind global events for scrolling.
1685 * @param {boolean} [on] Bind global events
1687 * @return {OO.ui.WindowManager} The manager, for chaining
1689 OO
.ui
.WindowManager
.prototype.toggleGlobalEvents = function ( on
) {
1690 var scrollWidth
, bodyMargin
,
1691 $body
= $( this.getElementDocument().body
),
1692 // We could have multiple window managers open so only modify
1693 // the body css at the bottom of the stack
1694 stackDepth
= $body
.data( 'windowManagerGlobalEvents' ) || 0;
1696 on
= on
=== undefined ? !!this.globalEvents
: !!on
;
1699 if ( !this.globalEvents
) {
1700 $( this.getElementWindow() ).on( {
1701 // Start listening for top-level window dimension changes
1702 'orientationchange resize': this.onWindowResizeHandler
1704 if ( stackDepth
=== 0 ) {
1705 scrollWidth
= window
.innerWidth
- document
.documentElement
.clientWidth
;
1706 bodyMargin
= parseFloat( $body
.css( 'margin-right' ) ) || 0;
1707 $body
.addClass( 'oo-ui-windowManager-modal-active' );
1708 $body
.css( 'margin-right', bodyMargin
+ scrollWidth
);
1711 this.globalEvents
= true;
1713 } else if ( this.globalEvents
) {
1714 $( this.getElementWindow() ).off( {
1715 // Stop listening for top-level window dimension changes
1716 'orientationchange resize': this.onWindowResizeHandler
1719 if ( stackDepth
=== 0 ) {
1720 $body
.removeClass( 'oo-ui-windowManager-modal-active' );
1721 $body
.css( 'margin-right', '' );
1723 this.globalEvents
= false;
1725 $body
.data( 'windowManagerGlobalEvents', stackDepth
);
1731 * Toggle screen reader visibility of content other than the window manager.
1734 * @param {boolean} [isolate] Make only the window manager visible to screen readers
1736 * @return {OO.ui.WindowManager} The manager, for chaining
1738 OO
.ui
.WindowManager
.prototype.toggleAriaIsolation = function ( isolate
) {
1739 var $topLevelElement
;
1740 isolate
= isolate
=== undefined ? !this.$ariaHidden
: !!isolate
;
1743 if ( !this.$ariaHidden
) {
1744 // Find the top level element containing the window manager or the
1745 // window manager's element itself in case its a direct child of body
1746 $topLevelElement
= this.$element
.parentsUntil( 'body' ).last();
1747 $topLevelElement
= $topLevelElement
.length
=== 0 ? this.$element
: $topLevelElement
;
1749 // In case previously set by another window manager
1750 this.$element
.removeAttr( 'aria-hidden' );
1752 // Hide everything other than the window manager from screen readers
1753 this.$ariaHidden
= $( document
.body
)
1756 .not( $topLevelElement
)
1757 .attr( 'aria-hidden', true );
1759 } else if ( this.$ariaHidden
) {
1760 // Restore screen reader visibility
1761 this.$ariaHidden
.removeAttr( 'aria-hidden' );
1762 this.$ariaHidden
= null;
1764 // and hide the window manager
1765 this.$element
.attr( 'aria-hidden', true );
1772 * Destroy the window manager.
1774 * Destroying the window manager ensures that it will no longer listen to events. If you would like
1775 * to continue using the window manager, but wish to remove all windows from it, use the
1776 * #clearWindows method instead.
1778 OO
.ui
.WindowManager
.prototype.destroy = function () {
1779 this.toggleGlobalEvents( false );
1780 this.toggleAriaIsolation( false );
1781 this.clearWindows();
1782 this.$element
.remove();
1786 * A window is a container for elements that are in a child frame. They are used with
1787 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1788 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’,
1789 * ‘medium’, ‘large’), which is interpreted by the window manager. If the requested size is not
1790 * recognized, the window manager will choose a sensible fallback.
1792 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1793 * different processes are executed:
1795 * **opening**: The opening stage begins when the window manager's
1796 * {@link OO.ui.WindowManager#openWindow openWindow} or the window's {@link #open open} methods are
1797 * used, and the window manager begins to open the window.
1799 * - {@link #getSetupProcess} method is called and its result executed
1800 * - {@link #getReadyProcess} method is called and its result executed
1802 * **opened**: The window is now open
1804 * **closing**: The closing stage begins when the window manager's
1805 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1806 * or the window's {@link #close} methods are used, and the window manager begins to close the
1809 * - {@link #getHoldProcess} method is called and its result executed
1810 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1812 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1813 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and
1814 * #getTeardownProcess methods. Note that each {@link OO.ui.Process process} is executed in series,
1815 * so asynchronous processing can complete. Always assume window processes are executed
1818 * For more information, please see the [OOUI documentation on MediaWiki] [1].
1820 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows
1824 * @extends OO.ui.Element
1825 * @mixins OO.EventEmitter
1828 * @param {Object} [config] Configuration options
1829 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1830 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
1832 OO
.ui
.Window
= function OoUiWindow( config
) {
1833 // Configuration initialization
1834 config
= config
|| {};
1836 // Parent constructor
1837 OO
.ui
.Window
.parent
.call( this, config
);
1839 // Mixin constructors
1840 OO
.EventEmitter
.call( this );
1843 this.manager
= null;
1844 this.size
= config
.size
|| this.constructor.static.size
;
1845 this.$frame
= $( '<div>' );
1847 * Overlay element to use for the `$overlay` configuration option of widgets that support it.
1848 * Things put inside it are overlaid on top of the window and are not bound to its dimensions.
1849 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
1851 * MyDialog.prototype.initialize = function () {
1853 * var popupButton = new OO.ui.PopupButtonWidget( {
1854 * $overlay: this.$overlay,
1855 * label: 'Popup button',
1857 * $content: $( '<p>Popup content.</p><p>More content.</p><p>Yet more content.</p>' ),
1864 * @property {jQuery}
1866 this.$overlay
= $( '<div>' );
1867 this.$content
= $( '<div>' );
1869 this.$focusTrapBefore
= $( '<div>' ).prop( 'tabIndex', 0 );
1870 this.$focusTrapAfter
= $( '<div>' ).prop( 'tabIndex', 0 );
1871 this.$focusTraps
= this.$focusTrapBefore
.add( this.$focusTrapAfter
);
1874 this.$overlay
.addClass( 'oo-ui-window-overlay' );
1876 .addClass( 'oo-ui-window-content' )
1877 .attr( 'tabindex', 0 );
1879 .addClass( 'oo-ui-window-frame' )
1880 .append( this.$focusTrapBefore
, this.$content
, this.$focusTrapAfter
);
1882 .addClass( 'oo-ui-window' )
1883 .append( this.$frame
, this.$overlay
);
1885 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1886 // that reference properties not initialized at that time of parent class construction
1887 // TODO: Find a better way to handle post-constructor setup
1888 this.visible
= false;
1889 this.$element
.addClass( 'oo-ui-element-hidden' );
1894 OO
.inheritClass( OO
.ui
.Window
, OO
.ui
.Element
);
1895 OO
.mixinClass( OO
.ui
.Window
, OO
.EventEmitter
);
1897 /* Static Properties */
1900 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1902 * The static size is used if no #size is configured during construction.
1906 * @property {string}
1908 OO
.ui
.Window
.static.size
= 'medium';
1913 * Handle mouse down events.
1916 * @param {jQuery.Event} e Mouse down event
1917 * @return {OO.ui.Window} The window, for chaining
1919 OO
.ui
.Window
.prototype.onMouseDown = function ( e
) {
1920 // Prevent clicking on the click-block from stealing focus
1921 if ( e
.target
=== this.$element
[ 0 ] ) {
1927 * Check if the window has been initialized.
1929 * Initialization occurs when a window is added to a manager.
1931 * @return {boolean} Window has been initialized
1933 OO
.ui
.Window
.prototype.isInitialized = function () {
1934 return !!this.manager
;
1938 * Check if the window is visible.
1940 * @return {boolean} Window is visible
1942 OO
.ui
.Window
.prototype.isVisible = function () {
1943 return this.visible
;
1947 * Check if the window is opening.
1949 * This method is a wrapper around the window manager's
1950 * {@link OO.ui.WindowManager#isOpening isOpening} method.
1952 * @return {boolean} Window is opening
1954 OO
.ui
.Window
.prototype.isOpening = function () {
1955 return this.manager
.isOpening( this );
1959 * Check if the window is closing.
1961 * This method is a wrapper around the window manager's
1962 * {@link OO.ui.WindowManager#isClosing isClosing} method.
1964 * @return {boolean} Window is closing
1966 OO
.ui
.Window
.prototype.isClosing = function () {
1967 return this.manager
.isClosing( this );
1971 * Check if the window is opened.
1973 * This method is a wrapper around the window manager's
1974 * {@link OO.ui.WindowManager#isOpened isOpened} method.
1976 * @return {boolean} Window is opened
1978 OO
.ui
.Window
.prototype.isOpened = function () {
1979 return this.manager
.isOpened( this );
1983 * Get the window manager.
1985 * All windows must be attached to a window manager, which is used to open
1986 * and close the window and control its presentation.
1988 * @return {OO.ui.WindowManager} Manager of window
1990 OO
.ui
.Window
.prototype.getManager = function () {
1991 return this.manager
;
1995 * Get the symbolic name of the window size (e.g., `small` or `medium`).
1997 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
1999 OO
.ui
.Window
.prototype.getSize = function () {
2000 var viewport
= OO
.ui
.Element
.static.getDimensions( this.getElementWindow() ),
2001 sizes
= this.manager
.constructor.static.sizes
,
2004 if ( !sizes
[ size
] ) {
2005 size
= this.manager
.constructor.static.defaultSize
;
2007 if ( size
!== 'full' && viewport
.rect
.right
- viewport
.rect
.left
< sizes
[ size
].width
) {
2015 * Get the size properties associated with the current window size
2017 * @return {Object} Size properties
2019 OO
.ui
.Window
.prototype.getSizeProperties = function () {
2020 return this.manager
.constructor.static.sizes
[ this.getSize() ];
2024 * Disable transitions on window's frame for the duration of the callback function, then enable them
2028 * @param {Function} callback Function to call while transitions are disabled
2030 OO
.ui
.Window
.prototype.withoutSizeTransitions = function ( callback
) {
2031 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2032 // Disable transitions first, otherwise we'll get values from when the window was animating.
2033 // We need to build the transition CSS properties using these specific properties since
2034 // Firefox doesn't return anything useful when asked just for 'transition'.
2035 var oldTransition
= this.$frame
.css( 'transition-property' ) + ' ' +
2036 this.$frame
.css( 'transition-duration' ) + ' ' +
2037 this.$frame
.css( 'transition-timing-function' ) + ' ' +
2038 this.$frame
.css( 'transition-delay' );
2040 this.$frame
.css( 'transition', 'none' );
2043 // Force reflow to make sure the style changes done inside callback
2044 // really are not transitioned
2045 this.$frame
.height();
2046 this.$frame
.css( 'transition', oldTransition
);
2050 * Get the height of the full window contents (i.e., the window head, body and foot together).
2052 * What constitutes the head, body, and foot varies depending on the window type.
2053 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
2054 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
2055 * and special actions in the head, and dialog content in the body.
2057 * To get just the height of the dialog body, use the #getBodyHeight method.
2059 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
2061 OO
.ui
.Window
.prototype.getContentHeight = function () {
2064 bodyStyleObj
= this.$body
[ 0 ].style
,
2065 frameStyleObj
= this.$frame
[ 0 ].style
;
2067 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2068 // Disable transitions first, otherwise we'll get values from when the window was animating.
2069 this.withoutSizeTransitions( function () {
2070 var oldHeight
= frameStyleObj
.height
,
2071 oldPosition
= bodyStyleObj
.position
;
2072 frameStyleObj
.height
= '1px';
2073 // Force body to resize to new width
2074 bodyStyleObj
.position
= 'relative';
2075 bodyHeight
= win
.getBodyHeight();
2076 frameStyleObj
.height
= oldHeight
;
2077 bodyStyleObj
.position
= oldPosition
;
2081 // Add buffer for border
2082 ( this.$frame
.outerHeight() - this.$frame
.innerHeight() ) +
2083 // Use combined heights of children
2084 ( this.$head
.outerHeight( true ) + bodyHeight
+ this.$foot
.outerHeight( true ) )
2089 * Get the height of the window body.
2091 * To get the height of the full window contents (the window body, head, and foot together),
2092 * use #getContentHeight.
2094 * When this function is called, the window will temporarily have been resized
2095 * to height=1px, so .scrollHeight measurements can be taken accurately.
2097 * @return {number} Height of the window body in pixels
2099 OO
.ui
.Window
.prototype.getBodyHeight = function () {
2100 return this.$body
[ 0 ].scrollHeight
;
2104 * Get the directionality of the frame (right-to-left or left-to-right).
2106 * @return {string} Directionality: `'ltr'` or `'rtl'`
2108 OO
.ui
.Window
.prototype.getDir = function () {
2109 return OO
.ui
.Element
.static.getDir( this.$content
) || 'ltr';
2113 * Get the 'setup' process.
2115 * The setup process is used to set up a window for use in a particular context, based on the `data`
2116 * argument. This method is called during the opening phase of the window’s lifecycle (before the
2117 * opening animation). You can add elements to the window in this process or set their default
2120 * Override this method to add additional steps to the ‘setup’ process the parent method provides
2121 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2124 * To add window content that persists between openings, you may wish to use the #initialize method
2127 * @param {Object} [data] Window opening data
2128 * @return {OO.ui.Process} Setup process
2130 OO
.ui
.Window
.prototype.getSetupProcess = function () {
2131 return new OO
.ui
.Process();
2135 * Get the ‘ready’ process.
2137 * The ready process is used to ready a window for use in a particular context, based on the `data`
2138 * argument. This method is called during the opening phase of the window’s lifecycle, after the
2139 * window has been {@link #getSetupProcess setup} (after the opening animation). You can focus
2140 * elements in the window in this process, or open their dropdowns.
2142 * Override this method to add additional steps to the ‘ready’ process the parent method
2143 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2144 * methods of OO.ui.Process.
2146 * @param {Object} [data] Window opening data
2147 * @return {OO.ui.Process} Ready process
2149 OO
.ui
.Window
.prototype.getReadyProcess = function () {
2150 return new OO
.ui
.Process();
2154 * Get the 'hold' process.
2156 * The hold process is used to keep a window from being used in a particular context, based on the
2157 * `data` argument. This method is called during the closing phase of the window’s lifecycle (before
2158 * the closing animation). You can close dropdowns of elements in the window in this process, if
2159 * they do not get closed automatically.
2161 * Override this method to add additional steps to the 'hold' process the parent method provides
2162 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2165 * @param {Object} [data] Window closing data
2166 * @return {OO.ui.Process} Hold process
2168 OO
.ui
.Window
.prototype.getHoldProcess = function () {
2169 return new OO
.ui
.Process();
2173 * Get the ‘teardown’ process.
2175 * The teardown process is used to teardown a window after use. During teardown, user interactions
2176 * within the window are conveyed and the window is closed, based on the `data` argument. This
2177 * method is called during the closing phase of the window’s lifecycle (after the closing
2178 * animation). You can remove elements in the window in this process or clear their values.
2180 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2181 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2184 * @param {Object} [data] Window closing data
2185 * @return {OO.ui.Process} Teardown process
2187 OO
.ui
.Window
.prototype.getTeardownProcess = function () {
2188 return new OO
.ui
.Process();
2192 * Set the window manager.
2194 * This will cause the window to initialize. Calling it more than once will cause an error.
2196 * @param {OO.ui.WindowManager} manager Manager for this window
2197 * @throws {Error} An error is thrown if the method is called more than once
2199 * @return {OO.ui.Window} The window, for chaining
2201 OO
.ui
.Window
.prototype.setManager = function ( manager
) {
2202 if ( this.manager
) {
2203 throw new Error( 'Cannot set window manager, window already has a manager' );
2206 this.manager
= manager
;
2213 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2215 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2218 * @return {OO.ui.Window} The window, for chaining
2220 OO
.ui
.Window
.prototype.setSize = function ( size
) {
2227 * Update the window size.
2229 * @throws {Error} An error is thrown if the window is not attached to a window manager
2231 * @return {OO.ui.Window} The window, for chaining
2233 OO
.ui
.Window
.prototype.updateSize = function () {
2234 if ( !this.manager
) {
2235 throw new Error( 'Cannot update window size, must be attached to a manager' );
2238 this.manager
.updateWindowSize( this );
2244 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2245 * when the window is opening. In general, setDimensions should not be called directly.
2247 * To set the size of the window, use the #setSize method.
2249 * @param {Object} dim CSS dimension properties
2250 * @param {string|number} [dim.width] Width
2251 * @param {string|number} [dim.minWidth] Minimum width
2252 * @param {string|number} [dim.maxWidth] Maximum width
2253 * @param {string|number} [dim.height] Height, omit to set based on height of contents
2254 * @param {string|number} [dim.minHeight] Minimum height
2255 * @param {string|number} [dim.maxHeight] Maximum height
2257 * @return {OO.ui.Window} The window, for chaining
2259 OO
.ui
.Window
.prototype.setDimensions = function ( dim
) {
2262 styleObj
= this.$frame
[ 0 ].style
;
2264 // Calculate the height we need to set using the correct width
2265 if ( dim
.height
=== undefined ) {
2266 this.withoutSizeTransitions( function () {
2267 var oldWidth
= styleObj
.width
;
2268 win
.$frame
.css( 'width', dim
.width
|| '' );
2269 height
= win
.getContentHeight();
2270 styleObj
.width
= oldWidth
;
2273 height
= dim
.height
;
2277 width
: dim
.width
|| '',
2278 minWidth
: dim
.minWidth
|| '',
2279 maxWidth
: dim
.maxWidth
|| '',
2280 height
: height
|| '',
2281 minHeight
: dim
.minHeight
|| '',
2282 maxHeight
: dim
.maxHeight
|| ''
2289 * Initialize window contents.
2291 * Before the window is opened for the first time, #initialize is called so that content that
2292 * persists between openings can be added to the window.
2294 * To set up a window with new content each time the window opens, use #getSetupProcess.
2296 * @throws {Error} An error is thrown if the window is not attached to a window manager
2298 * @return {OO.ui.Window} The window, for chaining
2300 OO
.ui
.Window
.prototype.initialize = function () {
2301 if ( !this.manager
) {
2302 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2306 this.$head
= $( '<div>' );
2307 this.$body
= $( '<div>' );
2308 this.$foot
= $( '<div>' );
2309 this.$document
= $( this.getElementDocument() );
2312 this.$element
.on( 'mousedown', this.onMouseDown
.bind( this ) );
2315 this.$head
.addClass( 'oo-ui-window-head' );
2316 this.$body
.addClass( 'oo-ui-window-body' );
2317 this.$foot
.addClass( 'oo-ui-window-foot' );
2318 this.$content
.append( this.$head
, this.$body
, this.$foot
);
2324 * Called when someone tries to focus the hidden element at the end of the dialog.
2325 * Sends focus back to the start of the dialog.
2327 * @param {jQuery.Event} event Focus event
2329 OO
.ui
.Window
.prototype.onFocusTrapFocused = function ( event
) {
2330 var backwards
= this.$focusTrapBefore
.is( event
.target
),
2331 element
= OO
.ui
.findFocusable( this.$content
, backwards
);
2333 // There's a focusable element inside the content, at the front or
2334 // back depending on which focus trap we hit; select it.
2337 // There's nothing focusable inside the content. As a fallback,
2338 // this.$content is focusable, and focusing it will keep our focus
2339 // properly trapped. It's not a *meaningful* focus, since it's just
2340 // the content-div for the Window, but it's better than letting focus
2341 // escape into the page.
2342 this.$content
.trigger( 'focus' );
2349 * This method is a wrapper around a call to the window
2350 * manager’s {@link OO.ui.WindowManager#openWindow openWindow} method.
2352 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2354 * @param {Object} [data] Window opening data
2355 * @return {OO.ui.WindowInstance} See OO.ui.WindowManager#openWindow
2356 * @throws {Error} An error is thrown if the window is not attached to a window manager
2358 OO
.ui
.Window
.prototype.open = function ( data
) {
2359 if ( !this.manager
) {
2360 throw new Error( 'Cannot open window, must be attached to a manager' );
2363 return this.manager
.openWindow( this, data
);
2369 * This method is a wrapper around a call to the window
2370 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method.
2372 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2373 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2374 * the window closes.
2376 * @param {Object} [data] Window closing data
2377 * @return {OO.ui.WindowInstance} See OO.ui.WindowManager#closeWindow
2378 * @throws {Error} An error is thrown if the window is not attached to a window manager
2380 OO
.ui
.Window
.prototype.close = function ( data
) {
2381 if ( !this.manager
) {
2382 throw new Error( 'Cannot close window, must be attached to a manager' );
2385 return this.manager
.closeWindow( this, data
);
2391 * This is called by OO.ui.WindowManager during window opening (before the animation), and should
2392 * not be called directly by other systems.
2394 * @param {Object} [data] Window opening data
2395 * @return {jQuery.Promise} Promise resolved when window is setup
2397 OO
.ui
.Window
.prototype.setup = function ( data
) {
2400 this.toggle( true );
2402 this.focusTrapHandler
= OO
.ui
.bind( this.onFocusTrapFocused
, this );
2403 this.$focusTraps
.on( 'focus', this.focusTrapHandler
);
2405 return this.getSetupProcess( data
).execute().then( function () {
2407 // Force redraw by asking the browser to measure the elements' widths
2408 win
.$element
.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2409 win
.$content
.addClass( 'oo-ui-window-content-setup' ).width();
2416 * This is called by OO.ui.WindowManager during window opening (after the animation), and should not
2417 * be called directly by other systems.
2419 * @param {Object} [data] Window opening data
2420 * @return {jQuery.Promise} Promise resolved when window is ready
2422 OO
.ui
.Window
.prototype.ready = function ( data
) {
2425 this.$content
.trigger( 'focus' );
2426 return this.getReadyProcess( data
).execute().then( function () {
2427 // Force redraw by asking the browser to measure the elements' widths
2428 win
.$element
.addClass( 'oo-ui-window-ready' ).width();
2429 win
.$content
.addClass( 'oo-ui-window-content-ready' ).width();
2436 * This is called by OO.ui.WindowManager during window closing (before the animation), and should
2437 * not be called directly by other systems.
2439 * @param {Object} [data] Window closing data
2440 * @return {jQuery.Promise} Promise resolved when window is held
2442 OO
.ui
.Window
.prototype.hold = function ( data
) {
2445 return this.getHoldProcess( data
).execute().then( function () {
2446 // Get the focused element within the window's content
2447 var $focus
= win
.$content
.find(
2448 OO
.ui
.Element
.static.getDocument( win
.$content
).activeElement
2451 // Blur the focused element
2452 if ( $focus
.length
) {
2456 // Force redraw by asking the browser to measure the elements' widths
2457 win
.$element
.removeClass( 'oo-ui-window-ready oo-ui-window-setup' ).width();
2458 win
.$content
.removeClass( 'oo-ui-window-content-ready oo-ui-window-content-setup' ).width();
2465 * This is called by OO.ui.WindowManager during window closing (after the animation), and should not
2466 * be called directly by other systems.
2468 * @param {Object} [data] Window closing data
2469 * @return {jQuery.Promise} Promise resolved when window is torn down
2471 OO
.ui
.Window
.prototype.teardown = function ( data
) {
2474 return this.getTeardownProcess( data
).execute().then( function () {
2475 // Force redraw by asking the browser to measure the elements' widths
2476 win
.$element
.removeClass( 'oo-ui-window-active' ).width();
2478 win
.$focusTraps
.off( 'focus', win
.focusTrapHandler
);
2479 win
.toggle( false );
2484 * The Dialog class serves as the base class for the other types of dialogs.
2485 * Unless extended to include controls, the rendered dialog box is a simple window
2486 * that users can close by hitting the Escape key. Dialog windows are used with OO.ui.WindowManager,
2487 * which opens, closes, and controls the presentation of the window. See the
2488 * [OOUI documentation on MediaWiki] [1] for more information.
2491 * // A simple dialog window.
2492 * function MyDialog( config ) {
2493 * MyDialog.parent.call( this, config );
2495 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2496 * MyDialog.static.name = 'myDialog';
2497 * MyDialog.prototype.initialize = function () {
2498 * MyDialog.parent.prototype.initialize.call( this );
2499 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2500 * this.content.$element.append( '<p>A simple dialog window. Press Escape key to ' +
2502 * this.$body.append( this.content.$element );
2504 * MyDialog.prototype.getBodyHeight = function () {
2505 * return this.content.$element.outerHeight( true );
2507 * var myDialog = new MyDialog( {
2510 * // Create and append a window manager, which opens and closes the window.
2511 * var windowManager = new OO.ui.WindowManager();
2512 * $( document.body ).append( windowManager.$element );
2513 * windowManager.addWindows( [ myDialog ] );
2514 * // Open the window!
2515 * windowManager.openWindow( myDialog );
2517 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Dialogs
2521 * @extends OO.ui.Window
2522 * @mixins OO.ui.mixin.PendingElement
2525 * @param {Object} [config] Configuration options
2527 OO
.ui
.Dialog
= function OoUiDialog( config
) {
2528 // Parent constructor
2529 OO
.ui
.Dialog
.parent
.call( this, config
);
2531 // Mixin constructors
2532 OO
.ui
.mixin
.PendingElement
.call( this );
2535 this.actions
= new OO
.ui
.ActionSet();
2536 this.attachedActions
= [];
2537 this.currentAction
= null;
2538 this.onDialogKeyDownHandler
= this.onDialogKeyDown
.bind( this );
2541 this.actions
.connect( this, {
2542 click
: 'onActionClick',
2543 change
: 'onActionsChange'
2548 .addClass( 'oo-ui-dialog' )
2549 .attr( 'role', 'dialog' );
2554 OO
.inheritClass( OO
.ui
.Dialog
, OO
.ui
.Window
);
2555 OO
.mixinClass( OO
.ui
.Dialog
, OO
.ui
.mixin
.PendingElement
);
2557 /* Static Properties */
2560 * Symbolic name of dialog.
2562 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2563 * Please see the [OOUI documentation on MediaWiki] [3] for more information.
2565 * [3]: https://www.mediawiki.org/wiki/OOUI/Windows/Window_managers
2570 * @property {string}
2572 OO
.ui
.Dialog
.static.name
= '';
2577 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node,
2578 * or a function that will produce a Label node or string. The title can also be specified with data
2579 * passed to the constructor (see #getSetupProcess). In this case, the static value will be
2585 * @property {jQuery|string|Function}
2587 OO
.ui
.Dialog
.static.title
= '';
2590 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2592 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this
2593 * case, the static value will be overridden.
2595 * [2]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs#Action_sets
2599 * @property {Object[]}
2601 OO
.ui
.Dialog
.static.actions
= [];
2604 * Close the dialog when the Escape key is pressed.
2609 * @property {boolean}
2611 OO
.ui
.Dialog
.static.escapable
= true;
2616 * Handle frame document key down events.
2619 * @param {jQuery.Event} e Key down event
2621 OO
.ui
.Dialog
.prototype.onDialogKeyDown = function ( e
) {
2623 if ( e
.which
=== OO
.ui
.Keys
.ESCAPE
&& this.constructor.static.escapable
) {
2624 this.executeAction( '' );
2626 e
.stopPropagation();
2627 } else if ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) {
2628 actions
= this.actions
.get( { flags
: 'primary', visible
: true, disabled
: false } );
2629 if ( actions
.length
> 0 ) {
2630 this.executeAction( actions
[ 0 ].getAction() );
2632 e
.stopPropagation();
2638 * Handle action click events.
2641 * @param {OO.ui.ActionWidget} action Action that was clicked
2643 OO
.ui
.Dialog
.prototype.onActionClick = function ( action
) {
2644 if ( !this.isPending() ) {
2645 this.executeAction( action
.getAction() );
2650 * Handle actions change event.
2654 OO
.ui
.Dialog
.prototype.onActionsChange = function () {
2655 this.detachActions();
2656 if ( !this.isClosing() ) {
2657 this.attachActions();
2658 if ( !this.isOpening() ) {
2659 // If the dialog is currently opening, this will be called automatically soon.
2666 * Get the set of actions used by the dialog.
2668 * @return {OO.ui.ActionSet}
2670 OO
.ui
.Dialog
.prototype.getActions = function () {
2671 return this.actions
;
2675 * Get a process for taking action.
2677 * When you override this method, you can create a new OO.ui.Process and return it, or add
2678 * additional accept steps to the process the parent method provides using the
2679 * {@link OO.ui.Process#first 'first'} and {@link OO.ui.Process#next 'next'} methods of
2682 * @param {string} [action] Symbolic name of action
2683 * @return {OO.ui.Process} Action process
2685 OO
.ui
.Dialog
.prototype.getActionProcess = function ( action
) {
2686 return new OO
.ui
.Process()
2687 .next( function () {
2689 // An empty action always closes the dialog without data, which should always be
2690 // safe and make no changes
2699 * @param {Object} [data] Dialog opening data
2700 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2701 * the {@link #static-title static title}
2702 * @param {Object[]} [data.actions] List of configuration options for each
2703 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2705 OO
.ui
.Dialog
.prototype.getSetupProcess = function ( data
) {
2709 return OO
.ui
.Dialog
.parent
.prototype.getSetupProcess
.call( this, data
)
2710 .next( function () {
2711 var config
= this.constructor.static,
2712 actions
= data
.actions
!== undefined ? data
.actions
: config
.actions
,
2713 title
= data
.title
!== undefined ? data
.title
: config
.title
;
2715 this.title
.setLabel( title
).setTitle( title
);
2716 this.actions
.add( this.getActionWidgets( actions
) );
2718 this.$element
.on( 'keydown', this.onDialogKeyDownHandler
);
2725 OO
.ui
.Dialog
.prototype.getTeardownProcess = function ( data
) {
2727 return OO
.ui
.Dialog
.parent
.prototype.getTeardownProcess
.call( this, data
)
2728 .first( function () {
2729 this.$element
.off( 'keydown', this.onDialogKeyDownHandler
);
2731 this.actions
.clear();
2732 this.currentAction
= null;
2739 OO
.ui
.Dialog
.prototype.initialize = function () {
2741 OO
.ui
.Dialog
.parent
.prototype.initialize
.call( this );
2744 this.title
= new OO
.ui
.LabelWidget();
2747 this.$content
.addClass( 'oo-ui-dialog-content' );
2748 this.$element
.attr( 'aria-labelledby', this.title
.getElementId() );
2749 this.setPendingElement( this.$head
);
2753 * Get action widgets from a list of configs
2755 * @param {Object[]} actions Action widget configs
2756 * @return {OO.ui.ActionWidget[]} Action widgets
2758 OO
.ui
.Dialog
.prototype.getActionWidgets = function ( actions
) {
2759 var i
, len
, widgets
= [];
2760 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
2761 widgets
.push( this.getActionWidget( actions
[ i
] ) );
2767 * Get action widget from config
2769 * Override this method to change the action widget class used.
2771 * @param {Object} config Action widget config
2772 * @return {OO.ui.ActionWidget} Action widget
2774 OO
.ui
.Dialog
.prototype.getActionWidget = function ( config
) {
2775 return new OO
.ui
.ActionWidget( this.getActionWidgetConfig( config
) );
2779 * Get action widget config
2781 * Override this method to modify the action widget config
2783 * @param {Object} config Initial action widget config
2784 * @return {Object} Action widget config
2786 OO
.ui
.Dialog
.prototype.getActionWidgetConfig = function ( config
) {
2791 * Attach action actions.
2795 OO
.ui
.Dialog
.prototype.attachActions = function () {
2796 // Remember the list of potentially attached actions
2797 this.attachedActions
= this.actions
.get();
2801 * Detach action actions.
2805 * @return {OO.ui.Dialog} The dialog, for chaining
2807 OO
.ui
.Dialog
.prototype.detachActions = function () {
2810 // Detach all actions that may have been previously attached
2811 for ( i
= 0, len
= this.attachedActions
.length
; i
< len
; i
++ ) {
2812 this.attachedActions
[ i
].$element
.detach();
2814 this.attachedActions
= [];
2820 * Execute an action.
2822 * @param {string} action Symbolic name of action to execute
2823 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2825 OO
.ui
.Dialog
.prototype.executeAction = function ( action
) {
2827 this.currentAction
= action
;
2828 return this.getActionProcess( action
).execute()
2829 .always( this.popPending
.bind( this ) );
2833 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
2834 * consists of a header that contains the dialog title, a body with the message, and a footer that
2835 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
2836 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
2838 * There are two basic types of message dialogs, confirmation and alert:
2840 * - **confirmation**: the dialog title describes what a progressive action will do and the message
2841 * provides more details about the consequences.
2842 * - **alert**: the dialog title describes which event occurred and the message provides more
2843 * information about why the event occurred.
2845 * The MessageDialog class specifies two actions: ‘accept’, the primary
2846 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
2847 * passing along the selected action.
2849 * For more information and examples, please see the [OOUI documentation on MediaWiki][1].
2852 * // Example: Creating and opening a message dialog window.
2853 * var messageDialog = new OO.ui.MessageDialog();
2855 * // Create and append a window manager.
2856 * var windowManager = new OO.ui.WindowManager();
2857 * $( document.body ).append( windowManager.$element );
2858 * windowManager.addWindows( [ messageDialog ] );
2859 * // Open the window.
2860 * windowManager.openWindow( messageDialog, {
2861 * title: 'Basic message dialog',
2862 * message: 'This is the message'
2865 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Message_Dialogs
2868 * @extends OO.ui.Dialog
2871 * @param {Object} [config] Configuration options
2873 OO
.ui
.MessageDialog
= function OoUiMessageDialog( config
) {
2874 // Parent constructor
2875 OO
.ui
.MessageDialog
.parent
.call( this, config
);
2878 this.verticalActionLayout
= null;
2881 this.$element
.addClass( 'oo-ui-messageDialog' );
2886 OO
.inheritClass( OO
.ui
.MessageDialog
, OO
.ui
.Dialog
);
2888 /* Static Properties */
2894 OO
.ui
.MessageDialog
.static.name
= 'message';
2900 OO
.ui
.MessageDialog
.static.size
= 'small';
2905 * The title of a confirmation dialog describes what a progressive action will do. The
2906 * title of an alert dialog describes which event occurred.
2910 * @property {jQuery|string|Function|null}
2912 OO
.ui
.MessageDialog
.static.title
= null;
2915 * The message displayed in the dialog body.
2917 * A confirmation message describes the consequences of a progressive action. An alert
2918 * message describes why an event occurred.
2922 * @property {jQuery|string|Function|null}
2924 OO
.ui
.MessageDialog
.static.message
= null;
2930 OO
.ui
.MessageDialog
.static.actions
= [
2931 // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
2932 { action
: 'accept', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-accept' ), flags
: 'primary' },
2933 { action
: 'reject', label
: OO
.ui
.deferMsg( 'ooui-dialog-message-reject' ), flags
: 'safe' }
2939 * Toggle action layout between vertical and horizontal.
2942 * @param {boolean} [value] Layout actions vertically, omit to toggle
2944 * @return {OO.ui.MessageDialog} The dialog, for chaining
2946 OO
.ui
.MessageDialog
.prototype.toggleVerticalActionLayout = function ( value
) {
2947 value
= value
=== undefined ? !this.verticalActionLayout
: !!value
;
2949 if ( value
!== this.verticalActionLayout
) {
2950 this.verticalActionLayout
= value
;
2952 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value
)
2953 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value
);
2962 OO
.ui
.MessageDialog
.prototype.getActionProcess = function ( action
) {
2964 return new OO
.ui
.Process( function () {
2965 this.close( { action
: action
} );
2968 return OO
.ui
.MessageDialog
.parent
.prototype.getActionProcess
.call( this, action
);
2974 * @param {Object} [data] Dialog opening data
2975 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
2976 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
2977 * @param {string} [data.size] Symbolic name of the dialog size, see OO.ui.Window
2978 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
2981 OO
.ui
.MessageDialog
.prototype.getSetupProcess = function ( data
) {
2985 return OO
.ui
.MessageDialog
.parent
.prototype.getSetupProcess
.call( this, data
)
2986 .next( function () {
2987 this.title
.setLabel(
2988 data
.title
!== undefined ? data
.title
: this.constructor.static.title
2990 this.message
.setLabel(
2991 data
.message
!== undefined ? data
.message
: this.constructor.static.message
2993 this.size
= data
.size
!== undefined ? data
.size
: this.constructor.static.size
;
3000 OO
.ui
.MessageDialog
.prototype.getReadyProcess = function ( data
) {
3004 return OO
.ui
.MessageDialog
.parent
.prototype.getReadyProcess
.call( this, data
)
3005 .next( function () {
3006 // Focus the primary action button
3007 var actions
= this.actions
.get();
3008 actions
= actions
.filter( function ( action
) {
3009 return action
.getFlags().indexOf( 'primary' ) > -1;
3011 if ( actions
.length
> 0 ) {
3012 actions
[ 0 ].focus();
3020 OO
.ui
.MessageDialog
.prototype.getBodyHeight = function () {
3021 var bodyHeight
, oldOverflow
,
3022 $scrollable
= this.container
.$element
;
3024 oldOverflow
= $scrollable
[ 0 ].style
.overflow
;
3025 $scrollable
[ 0 ].style
.overflow
= 'hidden';
3027 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
3029 bodyHeight
= this.text
.$element
.outerHeight( true );
3030 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
3038 OO
.ui
.MessageDialog
.prototype.setDimensions = function ( dim
) {
3041 $scrollable
= this.container
.$element
;
3042 OO
.ui
.MessageDialog
.parent
.prototype.setDimensions
.call( this, dim
);
3044 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
3045 // Need to do it after transition completes (250ms), add 50ms just in case.
3046 setTimeout( function () {
3047 var oldOverflow
= $scrollable
[ 0 ].style
.overflow
,
3048 activeElement
= document
.activeElement
;
3050 $scrollable
[ 0 ].style
.overflow
= 'hidden';
3052 OO
.ui
.Element
.static.reconsiderScrollbars( $scrollable
[ 0 ] );
3054 // Check reconsiderScrollbars didn't destroy our focus, as we
3055 // are doing this after the ready process.
3056 if ( activeElement
&& activeElement
!== document
.activeElement
&& activeElement
.focus
) {
3057 activeElement
.focus();
3060 $scrollable
[ 0 ].style
.overflow
= oldOverflow
;
3063 dialog
.fitActions();
3064 // Wait for CSS transition to finish and do it again :(
3065 setTimeout( function () {
3066 dialog
.fitActions();
3075 OO
.ui
.MessageDialog
.prototype.initialize = function () {
3077 OO
.ui
.MessageDialog
.parent
.prototype.initialize
.call( this );
3080 this.$actions
= $( '<div>' );
3081 this.container
= new OO
.ui
.PanelLayout( {
3082 scrollable
: true, classes
: [ 'oo-ui-messageDialog-container' ]
3084 this.text
= new OO
.ui
.PanelLayout( {
3085 padded
: true, expanded
: false, classes
: [ 'oo-ui-messageDialog-text' ]
3087 this.message
= new OO
.ui
.LabelWidget( {
3088 classes
: [ 'oo-ui-messageDialog-message' ]
3092 this.title
.$element
.addClass( 'oo-ui-messageDialog-title' );
3093 this.$content
.addClass( 'oo-ui-messageDialog-content' );
3094 this.container
.$element
.append( this.text
.$element
);
3095 this.text
.$element
.append( this.title
.$element
, this.message
.$element
);
3096 this.$body
.append( this.container
.$element
);
3097 this.$actions
.addClass( 'oo-ui-messageDialog-actions' );
3098 this.$foot
.append( this.$actions
);
3104 OO
.ui
.MessageDialog
.prototype.getActionWidgetConfig = function ( config
) {
3106 return $.extend( {}, config
, { framed
: false } );
3112 OO
.ui
.MessageDialog
.prototype.attachActions = function () {
3113 var i
, len
, special
, others
;
3116 OO
.ui
.MessageDialog
.parent
.prototype.attachActions
.call( this );
3118 special
= this.actions
.getSpecial();
3119 others
= this.actions
.getOthers();
3121 if ( special
.safe
) {
3122 this.$actions
.append( special
.safe
.$element
);
3123 special
.safe
.toggleFramed( true );
3125 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
3126 this.$actions
.append( others
[ i
].$element
);
3127 others
[ i
].toggleFramed( true );
3129 if ( special
.primary
) {
3130 this.$actions
.append( special
.primary
.$element
);
3131 special
.primary
.toggleFramed( true );
3136 * Fit action actions into columns or rows.
3138 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
3142 OO
.ui
.MessageDialog
.prototype.fitActions = function () {
3144 previous
= this.verticalActionLayout
,
3145 actions
= this.actions
.get();
3148 this.toggleVerticalActionLayout( false );
3149 for ( i
= 0, len
= actions
.length
; i
< len
; i
++ ) {
3150 action
= actions
[ i
];
3151 if ( action
.$element
[ 0 ].scrollWidth
> action
.$element
[ 0 ].clientWidth
) {
3152 this.toggleVerticalActionLayout( true );
3157 // Move the body out of the way of the foot
3158 this.$body
.css( 'bottom', this.$foot
.outerHeight( true ) );
3160 if ( this.verticalActionLayout
!== previous
) {
3161 // We changed the layout, window height might need to be updated.
3167 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
3168 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
3169 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again
3170 * when relevant. The ProcessDialog class is always extended and customized with the actions and
3171 * content required for each process.
3173 * The process dialog box consists of a header that visually represents the ‘working’ state of long
3174 * processes with an animation. The header contains the dialog title as well as
3175 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
3176 * a ‘primary’ action on the right (e.g., ‘Done’).
3178 * Like other windows, the process dialog is managed by a
3179 * {@link OO.ui.WindowManager window manager}.
3180 * Please see the [OOUI documentation on MediaWiki][1] for more information and examples.
3183 * // Example: Creating and opening a process dialog window.
3184 * function MyProcessDialog( config ) {
3185 * MyProcessDialog.parent.call( this, config );
3187 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
3189 * MyProcessDialog.static.name = 'myProcessDialog';
3190 * MyProcessDialog.static.title = 'Process dialog';
3191 * MyProcessDialog.static.actions = [
3192 * { action: 'save', label: 'Done', flags: 'primary' },
3193 * { label: 'Cancel', flags: 'safe' }
3196 * MyProcessDialog.prototype.initialize = function () {
3197 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
3198 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
3199 * this.content.$element.append( '<p>This is a process dialog window. The header ' +
3200 * 'contains the title and two buttons: \'Cancel\' (a safe action) on the left and ' +
3201 * '\'Done\' (a primary action) on the right.</p>' );
3202 * this.$body.append( this.content.$element );
3204 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
3205 * var dialog = this;
3207 * return new OO.ui.Process( function () {
3208 * dialog.close( { action: action } );
3211 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
3214 * var windowManager = new OO.ui.WindowManager();
3215 * $( document.body ).append( windowManager.$element );
3217 * var dialog = new MyProcessDialog();
3218 * windowManager.addWindows( [ dialog ] );
3219 * windowManager.openWindow( dialog );
3221 * [1]: https://www.mediawiki.org/wiki/OOUI/Windows/Process_Dialogs
3225 * @extends OO.ui.Dialog
3228 * @param {Object} [config] Configuration options
3230 OO
.ui
.ProcessDialog
= function OoUiProcessDialog( config
) {
3231 // Parent constructor
3232 OO
.ui
.ProcessDialog
.parent
.call( this, config
);
3235 this.fitOnOpen
= false;
3238 this.$element
.addClass( 'oo-ui-processDialog' );
3239 if ( OO
.ui
.isMobile() ) {
3240 this.$element
.addClass( 'oo-ui-isMobile' );
3246 OO
.inheritClass( OO
.ui
.ProcessDialog
, OO
.ui
.Dialog
);
3251 * Handle dismiss button click events.
3257 OO
.ui
.ProcessDialog
.prototype.onDismissErrorButtonClick = function () {
3262 * Handle retry button click events.
3264 * Hides errors and then tries again.
3268 OO
.ui
.ProcessDialog
.prototype.onRetryButtonClick = function () {
3270 this.executeAction( this.currentAction
);
3276 OO
.ui
.ProcessDialog
.prototype.initialize = function () {
3278 OO
.ui
.ProcessDialog
.parent
.prototype.initialize
.call( this );
3281 this.$navigation
= $( '<div>' );
3282 this.$location
= $( '<div>' );
3283 this.$safeActions
= $( '<div>' );
3284 this.$primaryActions
= $( '<div>' );
3285 this.$otherActions
= $( '<div>' );
3286 this.dismissButton
= new OO
.ui
.ButtonWidget( {
3287 label
: OO
.ui
.msg( 'ooui-dialog-process-dismiss' )
3289 this.retryButton
= new OO
.ui
.ButtonWidget();
3290 this.$errors
= $( '<div>' );
3291 this.$errorsTitle
= $( '<div>' );
3294 this.dismissButton
.connect( this, {
3295 click
: 'onDismissErrorButtonClick'
3297 this.retryButton
.connect( this, {
3298 click
: 'onRetryButtonClick'
3300 this.title
.connect( this, {
3301 labelChange
: 'fitLabel'
3305 this.title
.$element
.addClass( 'oo-ui-processDialog-title' );
3307 .append( this.title
.$element
)
3308 .addClass( 'oo-ui-processDialog-location' );
3309 this.$safeActions
.addClass( 'oo-ui-processDialog-actions-safe' );
3310 this.$primaryActions
.addClass( 'oo-ui-processDialog-actions-primary' );
3311 this.$otherActions
.addClass( 'oo-ui-processDialog-actions-other' );
3313 .addClass( 'oo-ui-processDialog-errors-title' )
3314 .text( OO
.ui
.msg( 'ooui-dialog-process-error' ) );
3316 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
3317 .append( this.$errorsTitle
, this.dismissButton
.$element
, this.retryButton
.$element
);
3319 .addClass( 'oo-ui-processDialog-content' )
3320 .append( this.$errors
);
3322 .addClass( 'oo-ui-processDialog-navigation' )
3323 // Note: Order of appends below is important. These are in the order
3324 // we want tab to go through them. Display-order is handled entirely
3325 // by CSS absolute-positioning. As such, primary actions like "done"
3327 .append( this.$primaryActions
, this.$location
, this.$safeActions
);
3328 this.$head
.append( this.$navigation
);
3329 this.$foot
.append( this.$otherActions
);
3335 OO
.ui
.ProcessDialog
.prototype.getActionWidgetConfig = function ( config
) {
3336 function checkFlag( flag
) {
3337 return config
.flags
=== flag
||
3338 ( Array
.isArray( config
.flags
) && config
.flags
.indexOf( flag
) !== -1 );
3341 // Default to unframed.
3342 config
= $.extend( { framed
: true }, config
);
3343 if ( checkFlag( 'close' ) ) {
3344 // Change close buttons to icon only.
3347 invisibleLabel
: true
3349 } else if ( OO
.ui
.isMobile() && checkFlag( 'back' ) ) {
3350 // Change back buttons to icon only.
3353 invisibleLabel
: true
3363 OO
.ui
.ProcessDialog
.prototype.attachActions = function () {
3364 var i
, len
, other
, special
, others
;
3367 OO
.ui
.ProcessDialog
.parent
.prototype.attachActions
.call( this );
3369 special
= this.actions
.getSpecial();
3370 others
= this.actions
.getOthers();
3371 if ( special
.primary
) {
3372 this.$primaryActions
.append( special
.primary
.$element
);
3374 for ( i
= 0, len
= others
.length
; i
< len
; i
++ ) {
3375 other
= others
[ i
];
3376 this.$otherActions
.append( other
.$element
);
3378 if ( special
.safe
) {
3379 this.$safeActions
.append( special
.safe
.$element
);
3386 OO
.ui
.ProcessDialog
.prototype.executeAction = function ( action
) {
3388 return OO
.ui
.ProcessDialog
.parent
.prototype.executeAction
.call( this, action
)
3389 .fail( function ( errors
) {
3390 process
.showErrors( errors
|| [] );
3397 OO
.ui
.ProcessDialog
.prototype.setDimensions = function () {
3401 OO
.ui
.ProcessDialog
.parent
.prototype.setDimensions
.apply( this, arguments
);
3405 // If there are many actions, they might be shown on multiple lines. Their layout can change
3406 // when resizing the dialog and when changing the actions. Adjust the height of the footer to
3408 dialog
.$body
.css( 'bottom', dialog
.$foot
.outerHeight( true ) );
3409 // Wait for CSS transition to finish and do it again :(
3410 setTimeout( function () {
3411 dialog
.$body
.css( 'bottom', dialog
.$foot
.outerHeight( true ) );
3416 * Fit label between actions.
3420 * @return {OO.ui.MessageDialog} The dialog, for chaining
3422 OO
.ui
.ProcessDialog
.prototype.fitLabel = function () {
3423 var safeWidth
, primaryWidth
, biggerWidth
, labelWidth
, navigationWidth
, leftWidth
, rightWidth
,
3424 size
= this.getSizeProperties();
3426 if ( typeof size
.width
!== 'number' ) {
3427 if ( this.isOpened() ) {
3428 navigationWidth
= this.$head
.width() - 20;
3429 } else if ( this.isOpening() ) {
3430 if ( !this.fitOnOpen
) {
3431 // Size is relative and the dialog isn't open yet, so wait.
3432 // FIXME: This should ideally be handled by setup somehow.
3433 this.manager
.lifecycle
.opened
.done( this.fitLabel
.bind( this ) );
3434 this.fitOnOpen
= true;
3441 navigationWidth
= size
.width
- 20;
3444 safeWidth
= this.$safeActions
.width();
3445 primaryWidth
= this.$primaryActions
.width();
3446 biggerWidth
= Math
.max( safeWidth
, primaryWidth
);
3448 labelWidth
= this.title
.$element
.width();
3450 if ( !OO
.ui
.isMobile() && 2 * biggerWidth
+ labelWidth
< navigationWidth
) {
3451 // We have enough space to center the label
3452 leftWidth
= rightWidth
= biggerWidth
;
3454 // Let's hope we at least have enough space not to overlap, because we can't wrap
3456 if ( this.getDir() === 'ltr' ) {
3457 leftWidth
= safeWidth
;
3458 rightWidth
= primaryWidth
;
3460 leftWidth
= primaryWidth
;
3461 rightWidth
= safeWidth
;
3465 this.$location
.css( { paddingLeft
: leftWidth
, paddingRight
: rightWidth
} );
3471 * Handle errors that occurred during accept or reject processes.
3474 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
3476 OO
.ui
.ProcessDialog
.prototype.showErrors = function ( errors
) {
3477 var i
, len
, $item
, actions
,
3483 if ( errors
instanceof OO
.ui
.Error
) {
3484 errors
= [ errors
];
3487 for ( i
= 0, len
= errors
.length
; i
< len
; i
++ ) {
3488 if ( !errors
[ i
].isRecoverable() ) {
3489 recoverable
= false;
3491 if ( errors
[ i
].isWarning() ) {
3494 $item
= $( '<div>' )
3495 .addClass( 'oo-ui-processDialog-error' )
3496 .append( errors
[ i
].getMessage() );
3497 items
.push( $item
[ 0 ] );
3499 this.$errorItems
= $( items
);
3500 if ( recoverable
) {
3501 abilities
[ this.currentAction
] = true;
3502 // Copy the flags from the first matching action.
3503 actions
= this.actions
.get( { actions
: this.currentAction
} );
3504 if ( actions
.length
) {
3505 this.retryButton
.clearFlags().setFlags( actions
[ 0 ].getFlags() );
3508 abilities
[ this.currentAction
] = false;
3509 this.actions
.setAbilities( abilities
);
3512 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-continue' ) );
3514 this.retryButton
.setLabel( OO
.ui
.msg( 'ooui-dialog-process-retry' ) );
3516 this.retryButton
.toggle( recoverable
);
3517 this.$errorsTitle
.after( this.$errorItems
);
3518 this.$errors
.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
3526 OO
.ui
.ProcessDialog
.prototype.hideErrors = function () {
3527 this.$errors
.addClass( 'oo-ui-element-hidden' );
3528 if ( this.$errorItems
) {
3529 this.$errorItems
.remove();
3530 this.$errorItems
= null;
3537 OO
.ui
.ProcessDialog
.prototype.getTeardownProcess = function ( data
) {
3539 return OO
.ui
.ProcessDialog
.parent
.prototype.getTeardownProcess
.call( this, data
)
3540 .first( function () {
3541 // Make sure to hide errors.
3543 this.fitOnOpen
= false;
3552 * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
3556 * @return {OO.ui.WindowManager}
3558 OO
.ui
.getWindowManager = function () {
3559 if ( !OO
.ui
.windowManager
) {
3560 OO
.ui
.windowManager
= new OO
.ui
.WindowManager();
3561 $( document
.body
).append( OO
.ui
.windowManager
.$element
);
3562 OO
.ui
.windowManager
.addWindows( [ new OO
.ui
.MessageDialog() ] );
3564 return OO
.ui
.windowManager
;
3568 * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
3569 * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
3570 * has only one action button, labelled "OK", clicking it will simply close the dialog.
3572 * A window manager is created automatically when this function is called for the first time.
3575 * OO.ui.alert( 'Something happened!' ).done( function () {
3576 * console.log( 'User closed the dialog.' );
3579 * OO.ui.alert( 'Something larger happened!', { size: 'large' } );
3581 * @param {jQuery|string} text Message text to display
3582 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3583 * @return {jQuery.Promise} Promise resolved when the user closes the dialog
3585 OO
.ui
.alert = function ( text
, options
) {
3586 return OO
.ui
.getWindowManager().openWindow( 'message', $.extend( {
3588 actions
: [ OO
.ui
.MessageDialog
.static.actions
[ 0 ] ]
3589 }, options
) ).closed
.then( function () {
3595 * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
3596 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3597 * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
3598 * (labelled "Cancel").
3600 * A window manager is created automatically when this function is called for the first time.
3603 * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
3604 * if ( confirmed ) {
3605 * console.log( 'User clicked "OK"!' );
3607 * console.log( 'User clicked "Cancel" or closed the dialog.' );
3611 * @param {jQuery|string} text Message text to display
3612 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3613 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3614 * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
3617 OO
.ui
.confirm = function ( text
, options
) {
3618 return OO
.ui
.getWindowManager().openWindow( 'message', $.extend( {
3620 }, options
) ).closed
.then( function ( data
) {
3621 return !!( data
&& data
.action
=== 'accept' );
3626 * Display a quick modal prompt dialog, using a OO.ui.MessageDialog. While the dialog is open,
3627 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
3628 * dialog has a text input widget and two action buttons, one to confirm an operation
3629 * (labelled "OK") and one to cancel it (labelled "Cancel").
3631 * A window manager is created automatically when this function is called for the first time.
3634 * OO.ui.prompt( 'Choose a line to go to', {
3635 * textInput: { placeholder: 'Line number' }
3636 * } ).done( function ( result ) {
3637 * if ( result !== null ) {
3638 * console.log( 'User typed "' + result + '" then clicked "OK".' );
3640 * console.log( 'User clicked "Cancel" or closed the dialog.' );
3644 * @param {jQuery|string} text Message text to display
3645 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
3646 * @param {Object} [options.textInput] Additional options for text input widget,
3647 * see OO.ui.TextInputWidget
3648 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
3649 * confirm, the promise will resolve with the value of the text input widget; otherwise, it will
3650 * resolve to `null`.
3652 OO
.ui
.prompt = function ( text
, options
) {
3654 manager
= OO
.ui
.getWindowManager(),
3655 textInput
= new OO
.ui
.TextInputWidget( ( options
&& options
.textInput
) || {} ),
3656 textField
= new OO
.ui
.FieldLayout( textInput
, {
3661 instance
= manager
.openWindow( 'message', $.extend( {
3662 message
: textField
.$element
3665 // TODO: This is a little hacky, and could be done by extending MessageDialog instead.
3666 instance
.opened
.then( function () {
3667 textInput
.on( 'enter', function () {
3668 manager
.getCurrentWindow().close( { action
: 'accept' } );
3673 return instance
.closed
.then( function ( data
) {
3674 return data
&& data
.action
=== 'accept' ? textInput
.getValue() : null;
3680 //# sourceMappingURL=oojs-ui-windows.js.map.json