Merge "DatabaseMssql: Don't duplicate body of makeList()"
[mediawiki.git] / resources / lib / oojs-ui / oojs-ui.js
blob0ad88fe19f95aa2c5699f8ec820d41ac981499d3
1 /*!
2  * OOjs UI v0.8.0
3  * https://www.mediawiki.org/wiki/OOjs_UI
4  *
5  * Copyright 2011–2015 OOjs Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2015-02-19T01:33:11Z
10  */
11 ( function ( OO ) {
13 'use strict';
15 /**
16  * Namespace for all classes, static methods and static properties.
17  *
18  * @class
19  * @singleton
20  */
21 OO.ui = {};
23 OO.ui.bind = $.proxy;
25 /**
26  * @property {Object}
27  */
28 OO.ui.Keys = {
29         UNDEFINED: 0,
30         BACKSPACE: 8,
31         DELETE: 46,
32         LEFT: 37,
33         RIGHT: 39,
34         UP: 38,
35         DOWN: 40,
36         ENTER: 13,
37         END: 35,
38         HOME: 36,
39         TAB: 9,
40         PAGEUP: 33,
41         PAGEDOWN: 34,
42         ESCAPE: 27,
43         SHIFT: 16,
44         SPACE: 32
47 /**
48  * Get the user's language and any fallback languages.
49  *
50  * These language codes are used to localize user interface elements in the user's language.
51  *
52  * In environments that provide a localization system, this function should be overridden to
53  * return the user's language(s). The default implementation returns English (en) only.
54  *
55  * @return {string[]} Language codes, in descending order of priority
56  */
57 OO.ui.getUserLanguages = function () {
58         return [ 'en' ];
61 /**
62  * Get a value in an object keyed by language code.
63  *
64  * @param {Object.<string,Mixed>} obj Object keyed by language code
65  * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
66  * @param {string} [fallback] Fallback code, used if no matching language can be found
67  * @return {Mixed} Local value
68  */
69 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
70         var i, len, langs;
72         // Requested language
73         if ( obj[ lang ] ) {
74                 return obj[ lang ];
75         }
76         // Known user language
77         langs = OO.ui.getUserLanguages();
78         for ( i = 0, len = langs.length; i < len; i++ ) {
79                 lang = langs[ i ];
80                 if ( obj[ lang ] ) {
81                         return obj[ lang ];
82                 }
83         }
84         // Fallback language
85         if ( obj[ fallback ] ) {
86                 return obj[ fallback ];
87         }
88         // First existing language
89         for ( lang in obj ) {
90                 return obj[ lang ];
91         }
93         return undefined;
96 /**
97  * Check if a node is contained within another node
98  *
99  * Similar to jQuery#contains except a list of containers can be supplied
100  * and a boolean argument allows you to include the container in the match list
102  * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
103  * @param {HTMLElement} contained Node to find
104  * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
105  * @return {boolean} The node is in the list of target nodes
106  */
107 OO.ui.contains = function ( containers, contained, matchContainers ) {
108         var i;
109         if ( !Array.isArray( containers ) ) {
110                 containers = [ containers ];
111         }
112         for ( i = containers.length - 1; i >= 0; i-- ) {
113                 if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
114                         return true;
115                 }
116         }
117         return false;
120 ( function () {
121         /**
122          * Message store for the default implementation of OO.ui.msg
123          *
124          * Environments that provide a localization system should not use this, but should override
125          * OO.ui.msg altogether.
126          *
127          * @private
128          */
129         var messages = {
130                 // Tool tip for a button that moves items in a list down one place
131                 'ooui-outline-control-move-down': 'Move item down',
132                 // Tool tip for a button that moves items in a list up one place
133                 'ooui-outline-control-move-up': 'Move item up',
134                 // Tool tip for a button that removes items from a list
135                 'ooui-outline-control-remove': 'Remove item',
136                 // Label for the toolbar group that contains a list of all other available tools
137                 'ooui-toolbar-more': 'More',
138                 // Label for the fake tool that expands the full list of tools in a toolbar group
139                 'ooui-toolgroup-expand': 'More',
140                 // Label for the fake tool that collapses the full list of tools in a toolbar group
141                 'ooui-toolgroup-collapse': 'Fewer',
142                 // Default label for the accept button of a confirmation dialog
143                 'ooui-dialog-message-accept': 'OK',
144                 // Default label for the reject button of a confirmation dialog
145                 'ooui-dialog-message-reject': 'Cancel',
146                 // Title for process dialog error description
147                 'ooui-dialog-process-error': 'Something went wrong',
148                 // Label for process dialog dismiss error button, visible when describing errors
149                 'ooui-dialog-process-dismiss': 'Dismiss',
150                 // Label for process dialog retry action button, visible when describing only recoverable errors
151                 'ooui-dialog-process-retry': 'Try again',
152                 // Label for process dialog retry action button, visible when describing only warnings
153                 'ooui-dialog-process-continue': 'Continue'
154         };
156         /**
157          * Get a localized message.
158          *
159          * In environments that provide a localization system, this function should be overridden to
160          * return the message translated in the user's language. The default implementation always returns
161          * English messages.
162          *
163          * After the message key, message parameters may optionally be passed. In the default implementation,
164          * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
165          * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
166          * they support unnamed, ordered message parameters.
167          *
168          * @abstract
169          * @param {string} key Message key
170          * @param {Mixed...} [params] Message parameters
171          * @return {string} Translated message with parameters substituted
172          */
173         OO.ui.msg = function ( key ) {
174                 var message = messages[ key ],
175                         params = Array.prototype.slice.call( arguments, 1 );
176                 if ( typeof message === 'string' ) {
177                         // Perform $1 substitution
178                         message = message.replace( /\$(\d+)/g, function ( unused, n ) {
179                                 var i = parseInt( n, 10 );
180                                 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
181                         } );
182                 } else {
183                         // Return placeholder if message not found
184                         message = '[' + key + ']';
185                 }
186                 return message;
187         };
189         /**
190          * Package a message and arguments for deferred resolution.
191          *
192          * Use this when you are statically specifying a message and the message may not yet be present.
193          *
194          * @param {string} key Message key
195          * @param {Mixed...} [params] Message parameters
196          * @return {Function} Function that returns the resolved message when executed
197          */
198         OO.ui.deferMsg = function () {
199                 var args = arguments;
200                 return function () {
201                         return OO.ui.msg.apply( OO.ui, args );
202                 };
203         };
205         /**
206          * Resolve a message.
207          *
208          * If the message is a function it will be executed, otherwise it will pass through directly.
209          *
210          * @param {Function|string} msg Deferred message, or message text
211          * @return {string} Resolved message
212          */
213         OO.ui.resolveMsg = function ( msg ) {
214                 if ( $.isFunction( msg ) ) {
215                         return msg();
216                 }
217                 return msg;
218         };
220 } )();
223  * Element that can be marked as pending.
225  * @abstract
226  * @class
228  * @constructor
229  * @param {Object} [config] Configuration options
230  * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
231  */
232 OO.ui.PendingElement = function OoUiPendingElement( config ) {
233         // Configuration initialization
234         config = config || {};
236         // Properties
237         this.pending = 0;
238         this.$pending = null;
240         // Initialisation
241         this.setPendingElement( config.$pending || this.$element );
244 /* Setup */
246 OO.initClass( OO.ui.PendingElement );
248 /* Methods */
251  * Set the pending element (and clean up any existing one).
253  * @param {jQuery} $pending The element to set to pending.
254  */
255 OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) {
256         if ( this.$pending ) {
257                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
258         }
260         this.$pending = $pending;
261         if ( this.pending > 0 ) {
262                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
263         }
267  * Check if input is pending.
269  * @return {boolean}
270  */
271 OO.ui.PendingElement.prototype.isPending = function () {
272         return !!this.pending;
276  * Increase the pending stack.
278  * @chainable
279  */
280 OO.ui.PendingElement.prototype.pushPending = function () {
281         if ( this.pending === 0 ) {
282                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
283                 this.updateThemeClasses();
284         }
285         this.pending++;
287         return this;
291  * Reduce the pending stack.
293  * Clamped at zero.
295  * @chainable
296  */
297 OO.ui.PendingElement.prototype.popPending = function () {
298         if ( this.pending === 1 ) {
299                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
300                 this.updateThemeClasses();
301         }
302         this.pending = Math.max( 0, this.pending - 1 );
304         return this;
308  * ActionSets manage the behavior of the {@link OO.ui.ActionWidget Action widgets} that comprise them.
309  * Actions can be made available for specific contexts (modes) and circumstances
310  * (abilities). Please see the [OOjs UI documentation on MediaWiki][1] for more information.
312  *     @example
313  *     // Example: An action set used in a process dialog
314  *     function ProcessDialog( config ) {
315  *         ProcessDialog.super.call( this, config );
316  *     }
317  *     OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
318  *     ProcessDialog.static.title = 'An action set in a process dialog';
319  *     // An action set that uses modes ('edit' and 'help' mode, in this example).
320  *     ProcessDialog.static.actions = [
321  *        { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
322  *        { action: 'help', modes: 'edit', label: 'Help' },
323  *        { modes: 'edit', label: 'Cancel', flags: 'safe' },
324  *        { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
325  *     ];
327  *     ProcessDialog.prototype.initialize = function () {
328  *         ProcessDialog.super.prototype.initialize.apply( this, arguments );
329  *         this.panel1 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
330  *         this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode. </p>' );
331  *         this.panel2 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
332  *         this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode</p>' );
333  *         this.stackLayout= new OO.ui.StackLayout( {
334  *             items: [ this.panel1, this.panel2 ]
335  *         });
336  *         this.$body.append( this.stackLayout.$element );
337  *     };
338  *     ProcessDialog.prototype.getSetupProcess = function ( data ) {
339  *         return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
340  *         .next( function () {
341  *         this.actions.setMode('edit');
342  *         }, this );
343  *     };
344  *     ProcessDialog.prototype.getActionProcess = function ( action ) {
345  *         if ( action === 'help' ) {
346  *             this.actions.setMode( 'help' );
347  *             this.stackLayout.setItem( this.panel2 );
348  *             } else if ( action === 'back' ) {
349  *             this.actions.setMode( 'edit' );
350  *             this.stackLayout.setItem( this.panel1 );
351  *             } else if ( action === 'continue' ) {
352  *             var dialog = this;
353  *             return new OO.ui.Process( function () {
354  *                 dialog.close();
355  *             } );
356  *         }
357  *         return ProcessDialog.super.prototype.getActionProcess.call( this, action );
358  *     };
359  *     ProcessDialog.prototype.getBodyHeight = function () {
360  *         return this.panel1.$element.outerHeight( true );
361  *     };
362  *     var windowManager = new OO.ui.WindowManager();
363  *     $( 'body' ).append( windowManager.$element );
364  *     var processDialog = new ProcessDialog({
365  *        size: 'medium'});
366  *     windowManager.addWindows( [ processDialog ] );
367  *     windowManager.openWindow( processDialog );
369  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
371  * @abstract
372  * @class
373  * @mixins OO.EventEmitter
375  * @constructor
376  * @param {Object} [config] Configuration options
377  */
378 OO.ui.ActionSet = function OoUiActionSet( config ) {
379         // Configuration initialization
380         config = config || {};
382         // Mixin constructors
383         OO.EventEmitter.call( this );
385         // Properties
386         this.list = [];
387         this.categories = {
388                 actions: 'getAction',
389                 flags: 'getFlags',
390                 modes: 'getModes'
391         };
392         this.categorized = {};
393         this.special = {};
394         this.others = [];
395         this.organized = false;
396         this.changing = false;
397         this.changed = false;
400 /* Setup */
402 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
404 /* Static Properties */
407  * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
408  *  header of a {@link OO.ui.ProcessDialog process dialog}.
409  *  See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
411  *  [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
413  * @abstract
414  * @static
415  * @inheritable
416  * @property {string}
417  */
418 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
420 /* Events */
423  * @event click
424  * @param {OO.ui.ActionWidget} action Action that was clicked
425  */
428  * @event resize
429  * @param {OO.ui.ActionWidget} action Action that was resized
430  */
433  * @event add
434  * @param {OO.ui.ActionWidget[]} added Actions added
435  */
438  * @event remove
439  * @param {OO.ui.ActionWidget[]} added Actions removed
440  */
443  * @event change
444  */
446 /* Methods */
449  * Handle action change events.
451  * @private
452  * @fires change
453  */
454 OO.ui.ActionSet.prototype.onActionChange = function () {
455         this.organized = false;
456         if ( this.changing ) {
457                 this.changed = true;
458         } else {
459                 this.emit( 'change' );
460         }
464  * Check if a action is one of the special actions.
466  * @param {OO.ui.ActionWidget} action Action to check
467  * @return {boolean} Action is special
468  */
469 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
470         var flag;
472         for ( flag in this.special ) {
473                 if ( action === this.special[ flag ] ) {
474                         return true;
475                 }
476         }
478         return false;
482  * Get actions.
484  * @param {Object} [filters] Filters to use, omit to get all actions
485  * @param {string|string[]} [filters.actions] Actions that actions must have
486  * @param {string|string[]} [filters.flags] Flags that actions must have
487  * @param {string|string[]} [filters.modes] Modes that actions must have
488  * @param {boolean} [filters.visible] Actions must be visible
489  * @param {boolean} [filters.disabled] Actions must be disabled
490  * @return {OO.ui.ActionWidget[]} Actions matching all criteria
491  */
492 OO.ui.ActionSet.prototype.get = function ( filters ) {
493         var i, len, list, category, actions, index, match, matches;
495         if ( filters ) {
496                 this.organize();
498                 // Collect category candidates
499                 matches = [];
500                 for ( category in this.categorized ) {
501                         list = filters[ category ];
502                         if ( list ) {
503                                 if ( !Array.isArray( list ) ) {
504                                         list = [ list ];
505                                 }
506                                 for ( i = 0, len = list.length; i < len; i++ ) {
507                                         actions = this.categorized[ category ][ list[ i ] ];
508                                         if ( Array.isArray( actions ) ) {
509                                                 matches.push.apply( matches, actions );
510                                         }
511                                 }
512                         }
513                 }
514                 // Remove by boolean filters
515                 for ( i = 0, len = matches.length; i < len; i++ ) {
516                         match = matches[ i ];
517                         if (
518                                 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
519                                 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
520                         ) {
521                                 matches.splice( i, 1 );
522                                 len--;
523                                 i--;
524                         }
525                 }
526                 // Remove duplicates
527                 for ( i = 0, len = matches.length; i < len; i++ ) {
528                         match = matches[ i ];
529                         index = matches.lastIndexOf( match );
530                         while ( index !== i ) {
531                                 matches.splice( index, 1 );
532                                 len--;
533                                 index = matches.lastIndexOf( match );
534                         }
535                 }
536                 return matches;
537         }
538         return this.list.slice();
542  * Get special actions.
544  * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
545  * Special flags can be configured by changing #static-specialFlags in a subclass.
547  * @return {OO.ui.ActionWidget|null} Safe action
548  */
549 OO.ui.ActionSet.prototype.getSpecial = function () {
550         this.organize();
551         return $.extend( {}, this.special );
555  * Get other actions.
557  * Other actions include all non-special visible actions.
559  * @return {OO.ui.ActionWidget[]} Other actions
560  */
561 OO.ui.ActionSet.prototype.getOthers = function () {
562         this.organize();
563         return this.others.slice();
567  * Toggle actions based on their modes.
569  * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
570  * visibility; matching actions will be shown, non-matching actions will be hidden.
572  * @param {string} mode Mode actions must have
573  * @chainable
574  * @fires toggle
575  * @fires change
576  */
577 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
578         var i, len, action;
580         this.changing = true;
581         for ( i = 0, len = this.list.length; i < len; i++ ) {
582                 action = this.list[ i ];
583                 action.toggle( action.hasMode( mode ) );
584         }
586         this.organized = false;
587         this.changing = false;
588         this.emit( 'change' );
590         return this;
594  * Change which actions are able to be performed.
596  * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
598  * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
599  *   indicate actions are able to be performed
600  * @chainable
601  */
602 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
603         var i, len, action, item;
605         for ( i = 0, len = this.list.length; i < len; i++ ) {
606                 item = this.list[ i ];
607                 action = item.getAction();
608                 if ( actions[ action ] !== undefined ) {
609                         item.setDisabled( !actions[ action ] );
610                 }
611         }
613         return this;
617  * Executes a function once per action.
619  * When making changes to multiple actions, use this method instead of iterating over the actions
620  * manually to defer emitting a change event until after all actions have been changed.
622  * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
623  * @param {Function} callback Callback to run for each action; callback is invoked with three
624  *   arguments: the action, the action's index, the list of actions being iterated over
625  * @chainable
626  */
627 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
628         this.changed = false;
629         this.changing = true;
630         this.get( filter ).forEach( callback );
631         this.changing = false;
632         if ( this.changed ) {
633                 this.emit( 'change' );
634         }
636         return this;
640  * Add actions.
642  * @param {OO.ui.ActionWidget[]} actions Actions to add
643  * @chainable
644  * @fires add
645  * @fires change
646  */
647 OO.ui.ActionSet.prototype.add = function ( actions ) {
648         var i, len, action;
650         this.changing = true;
651         for ( i = 0, len = actions.length; i < len; i++ ) {
652                 action = actions[ i ];
653                 action.connect( this, {
654                         click: [ 'emit', 'click', action ],
655                         resize: [ 'emit', 'resize', action ],
656                         toggle: [ 'onActionChange' ]
657                 } );
658                 this.list.push( action );
659         }
660         this.organized = false;
661         this.emit( 'add', actions );
662         this.changing = false;
663         this.emit( 'change' );
665         return this;
669  * Remove actions.
671  * @param {OO.ui.ActionWidget[]} actions Actions to remove
672  * @chainable
673  * @fires remove
674  * @fires change
675  */
676 OO.ui.ActionSet.prototype.remove = function ( actions ) {
677         var i, len, index, action;
679         this.changing = true;
680         for ( i = 0, len = actions.length; i < len; i++ ) {
681                 action = actions[ i ];
682                 index = this.list.indexOf( action );
683                 if ( index !== -1 ) {
684                         action.disconnect( this );
685                         this.list.splice( index, 1 );
686                 }
687         }
688         this.organized = false;
689         this.emit( 'remove', actions );
690         this.changing = false;
691         this.emit( 'change' );
693         return this;
697  * Remove all actions.
699  * @chainable
700  * @fires remove
701  * @fires change
702  */
703 OO.ui.ActionSet.prototype.clear = function () {
704         var i, len, action,
705                 removed = this.list.slice();
707         this.changing = true;
708         for ( i = 0, len = this.list.length; i < len; i++ ) {
709                 action = this.list[ i ];
710                 action.disconnect( this );
711         }
713         this.list = [];
715         this.organized = false;
716         this.emit( 'remove', removed );
717         this.changing = false;
718         this.emit( 'change' );
720         return this;
724  * Organize actions.
726  * This is called whenever organized information is requested. It will only reorganize the actions
727  * if something has changed since the last time it ran.
729  * @private
730  * @chainable
731  */
732 OO.ui.ActionSet.prototype.organize = function () {
733         var i, iLen, j, jLen, flag, action, category, list, item, special,
734                 specialFlags = this.constructor.static.specialFlags;
736         if ( !this.organized ) {
737                 this.categorized = {};
738                 this.special = {};
739                 this.others = [];
740                 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
741                         action = this.list[ i ];
742                         if ( action.isVisible() ) {
743                                 // Populate categories
744                                 for ( category in this.categories ) {
745                                         if ( !this.categorized[ category ] ) {
746                                                 this.categorized[ category ] = {};
747                                         }
748                                         list = action[ this.categories[ category ] ]();
749                                         if ( !Array.isArray( list ) ) {
750                                                 list = [ list ];
751                                         }
752                                         for ( j = 0, jLen = list.length; j < jLen; j++ ) {
753                                                 item = list[ j ];
754                                                 if ( !this.categorized[ category ][ item ] ) {
755                                                         this.categorized[ category ][ item ] = [];
756                                                 }
757                                                 this.categorized[ category ][ item ].push( action );
758                                         }
759                                 }
760                                 // Populate special/others
761                                 special = false;
762                                 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
763                                         flag = specialFlags[ j ];
764                                         if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
765                                                 this.special[ flag ] = action;
766                                                 special = true;
767                                                 break;
768                                         }
769                                 }
770                                 if ( !special ) {
771                                         this.others.push( action );
772                                 }
773                         }
774                 }
775                 this.organized = true;
776         }
778         return this;
782  * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
783  * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
784  * connected to them and can't be interacted with.
786  * @abstract
787  * @class
789  * @constructor
790  * @param {Object} [config] Configuration options
791  * @cfg {string[]} [classes] CSS class names to add
792  * @cfg {string} [id] HTML id attribute
793  * @cfg {string} [text] Text to insert
794  * @cfg {jQuery} [$content] Content elements to append (after text)
795  * @cfg {Mixed} [data] Element data
796  */
797 OO.ui.Element = function OoUiElement( config ) {
798         // Configuration initialization
799         config = config || {};
801         // Properties
802         this.$ = $;
803         this.data = config.data;
804         this.$element = $( document.createElement( this.getTagName() ) );
805         this.elementGroup = null;
806         this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
807         this.updateThemeClassesPending = false;
809         // Initialization
810         if ( Array.isArray( config.classes ) ) {
811                 this.$element.addClass( config.classes.join( ' ' ) );
812         }
813         if ( config.id ) {
814                 this.$element.attr( 'id', config.id );
815         }
816         if ( config.text ) {
817                 this.$element.text( config.text );
818         }
819         if ( config.$content ) {
820                 this.$element.append( config.$content );
821         }
824 /* Setup */
826 OO.initClass( OO.ui.Element );
828 /* Static Properties */
831  * HTML tag name.
833  * This may be ignored if #getTagName is overridden.
835  * @static
836  * @inheritable
837  * @property {string}
838  */
839 OO.ui.Element.static.tagName = 'div';
841 /* Static Methods */
844  * Get a jQuery function within a specific document.
846  * @static
847  * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
848  * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
849  *   not in an iframe
850  * @return {Function} Bound jQuery function
851  */
852 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
853         function wrapper( selector ) {
854                 return $( selector, wrapper.context );
855         }
857         wrapper.context = this.getDocument( context );
859         if ( $iframe ) {
860                 wrapper.$iframe = $iframe;
861         }
863         return wrapper;
867  * Get the document of an element.
869  * @static
870  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
871  * @return {HTMLDocument|null} Document object
872  */
873 OO.ui.Element.static.getDocument = function ( obj ) {
874         // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
875         return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
876                 // Empty jQuery selections might have a context
877                 obj.context ||
878                 // HTMLElement
879                 obj.ownerDocument ||
880                 // Window
881                 obj.document ||
882                 // HTMLDocument
883                 ( obj.nodeType === 9 && obj ) ||
884                 null;
888  * Get the window of an element or document.
890  * @static
891  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
892  * @return {Window} Window object
893  */
894 OO.ui.Element.static.getWindow = function ( obj ) {
895         var doc = this.getDocument( obj );
896         return doc.parentWindow || doc.defaultView;
900  * Get the direction of an element or document.
902  * @static
903  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
904  * @return {string} Text direction, either 'ltr' or 'rtl'
905  */
906 OO.ui.Element.static.getDir = function ( obj ) {
907         var isDoc, isWin;
909         if ( obj instanceof jQuery ) {
910                 obj = obj[ 0 ];
911         }
912         isDoc = obj.nodeType === 9;
913         isWin = obj.document !== undefined;
914         if ( isDoc || isWin ) {
915                 if ( isWin ) {
916                         obj = obj.document;
917                 }
918                 obj = obj.body;
919         }
920         return $( obj ).css( 'direction' );
924  * Get the offset between two frames.
926  * TODO: Make this function not use recursion.
928  * @static
929  * @param {Window} from Window of the child frame
930  * @param {Window} [to=window] Window of the parent frame
931  * @param {Object} [offset] Offset to start with, used internally
932  * @return {Object} Offset object, containing left and top properties
933  */
934 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
935         var i, len, frames, frame, rect;
937         if ( !to ) {
938                 to = window;
939         }
940         if ( !offset ) {
941                 offset = { top: 0, left: 0 };
942         }
943         if ( from.parent === from ) {
944                 return offset;
945         }
947         // Get iframe element
948         frames = from.parent.document.getElementsByTagName( 'iframe' );
949         for ( i = 0, len = frames.length; i < len; i++ ) {
950                 if ( frames[ i ].contentWindow === from ) {
951                         frame = frames[ i ];
952                         break;
953                 }
954         }
956         // Recursively accumulate offset values
957         if ( frame ) {
958                 rect = frame.getBoundingClientRect();
959                 offset.left += rect.left;
960                 offset.top += rect.top;
961                 if ( from !== to ) {
962                         this.getFrameOffset( from.parent, offset );
963                 }
964         }
965         return offset;
969  * Get the offset between two elements.
971  * The two elements may be in a different frame, but in that case the frame $element is in must
972  * be contained in the frame $anchor is in.
974  * @static
975  * @param {jQuery} $element Element whose position to get
976  * @param {jQuery} $anchor Element to get $element's position relative to
977  * @return {Object} Translated position coordinates, containing top and left properties
978  */
979 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
980         var iframe, iframePos,
981                 pos = $element.offset(),
982                 anchorPos = $anchor.offset(),
983                 elementDocument = this.getDocument( $element ),
984                 anchorDocument = this.getDocument( $anchor );
986         // If $element isn't in the same document as $anchor, traverse up
987         while ( elementDocument !== anchorDocument ) {
988                 iframe = elementDocument.defaultView.frameElement;
989                 if ( !iframe ) {
990                         throw new Error( '$element frame is not contained in $anchor frame' );
991                 }
992                 iframePos = $( iframe ).offset();
993                 pos.left += iframePos.left;
994                 pos.top += iframePos.top;
995                 elementDocument = iframe.ownerDocument;
996         }
997         pos.left -= anchorPos.left;
998         pos.top -= anchorPos.top;
999         return pos;
1003  * Get element border sizes.
1005  * @static
1006  * @param {HTMLElement} el Element to measure
1007  * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1008  */
1009 OO.ui.Element.static.getBorders = function ( el ) {
1010         var doc = el.ownerDocument,
1011                 win = doc.parentWindow || doc.defaultView,
1012                 style = win && win.getComputedStyle ?
1013                         win.getComputedStyle( el, null ) :
1014                         el.currentStyle,
1015                 $el = $( el ),
1016                 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1017                 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1018                 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1019                 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1021         return {
1022                 top: top,
1023                 left: left,
1024                 bottom: bottom,
1025                 right: right
1026         };
1030  * Get dimensions of an element or window.
1032  * @static
1033  * @param {HTMLElement|Window} el Element to measure
1034  * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1035  */
1036 OO.ui.Element.static.getDimensions = function ( el ) {
1037         var $el, $win,
1038                 doc = el.ownerDocument || el.document,
1039                 win = doc.parentWindow || doc.defaultView;
1041         if ( win === el || el === doc.documentElement ) {
1042                 $win = $( win );
1043                 return {
1044                         borders: { top: 0, left: 0, bottom: 0, right: 0 },
1045                         scroll: {
1046                                 top: $win.scrollTop(),
1047                                 left: $win.scrollLeft()
1048                         },
1049                         scrollbar: { right: 0, bottom: 0 },
1050                         rect: {
1051                                 top: 0,
1052                                 left: 0,
1053                                 bottom: $win.innerHeight(),
1054                                 right: $win.innerWidth()
1055                         }
1056                 };
1057         } else {
1058                 $el = $( el );
1059                 return {
1060                         borders: this.getBorders( el ),
1061                         scroll: {
1062                                 top: $el.scrollTop(),
1063                                 left: $el.scrollLeft()
1064                         },
1065                         scrollbar: {
1066                                 right: $el.innerWidth() - el.clientWidth,
1067                                 bottom: $el.innerHeight() - el.clientHeight
1068                         },
1069                         rect: el.getBoundingClientRect()
1070                 };
1071         }
1075  * Get scrollable object parent
1077  * documentElement can't be used to get or set the scrollTop
1078  * property on Blink. Changing and testing its value lets us
1079  * use 'body' or 'documentElement' based on what is working.
1081  * https://code.google.com/p/chromium/issues/detail?id=303131
1083  * @static
1084  * @param {HTMLElement} el Element to find scrollable parent for
1085  * @return {HTMLElement} Scrollable parent
1086  */
1087 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1088         var scrollTop, body;
1090         if ( OO.ui.scrollableElement === undefined ) {
1091                 body = el.ownerDocument.body;
1092                 scrollTop = body.scrollTop;
1093                 body.scrollTop = 1;
1095                 if ( body.scrollTop === 1 ) {
1096                         body.scrollTop = scrollTop;
1097                         OO.ui.scrollableElement = 'body';
1098                 } else {
1099                         OO.ui.scrollableElement = 'documentElement';
1100                 }
1101         }
1103         return el.ownerDocument[ OO.ui.scrollableElement ];
1107  * Get closest scrollable container.
1109  * Traverses up until either a scrollable element or the root is reached, in which case the window
1110  * will be returned.
1112  * @static
1113  * @param {HTMLElement} el Element to find scrollable container for
1114  * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1115  * @return {HTMLElement} Closest scrollable container
1116  */
1117 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1118         var i, val,
1119                 props = [ 'overflow' ],
1120                 $parent = $( el ).parent();
1122         if ( dimension === 'x' || dimension === 'y' ) {
1123                 props.push( 'overflow-' + dimension );
1124         }
1126         while ( $parent.length ) {
1127                 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1128                         return $parent[ 0 ];
1129                 }
1130                 i = props.length;
1131                 while ( i-- ) {
1132                         val = $parent.css( props[ i ] );
1133                         if ( val === 'auto' || val === 'scroll' ) {
1134                                 return $parent[ 0 ];
1135                         }
1136                 }
1137                 $parent = $parent.parent();
1138         }
1139         return this.getDocument( el ).body;
1143  * Scroll element into view.
1145  * @static
1146  * @param {HTMLElement} el Element to scroll into view
1147  * @param {Object} [config] Configuration options
1148  * @param {string} [config.duration] jQuery animation duration value
1149  * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1150  *  to scroll in both directions
1151  * @param {Function} [config.complete] Function to call when scrolling completes
1152  */
1153 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1154         // Configuration initialization
1155         config = config || {};
1157         var rel, anim = {},
1158                 callback = typeof config.complete === 'function' && config.complete,
1159                 sc = this.getClosestScrollableContainer( el, config.direction ),
1160                 $sc = $( sc ),
1161                 eld = this.getDimensions( el ),
1162                 scd = this.getDimensions( sc ),
1163                 $win = $( this.getWindow( el ) );
1165         // Compute the distances between the edges of el and the edges of the scroll viewport
1166         if ( $sc.is( 'html, body' ) ) {
1167                 // If the scrollable container is the root, this is easy
1168                 rel = {
1169                         top: eld.rect.top,
1170                         bottom: $win.innerHeight() - eld.rect.bottom,
1171                         left: eld.rect.left,
1172                         right: $win.innerWidth() - eld.rect.right
1173                 };
1174         } else {
1175                 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1176                 rel = {
1177                         top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1178                         bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1179                         left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1180                         right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1181                 };
1182         }
1184         if ( !config.direction || config.direction === 'y' ) {
1185                 if ( rel.top < 0 ) {
1186                         anim.scrollTop = scd.scroll.top + rel.top;
1187                 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1188                         anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1189                 }
1190         }
1191         if ( !config.direction || config.direction === 'x' ) {
1192                 if ( rel.left < 0 ) {
1193                         anim.scrollLeft = scd.scroll.left + rel.left;
1194                 } else if ( rel.left > 0 && rel.right < 0 ) {
1195                         anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1196                 }
1197         }
1198         if ( !$.isEmptyObject( anim ) ) {
1199                 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1200                 if ( callback ) {
1201                         $sc.queue( function ( next ) {
1202                                 callback();
1203                                 next();
1204                         } );
1205                 }
1206         } else {
1207                 if ( callback ) {
1208                         callback();
1209                 }
1210         }
1214  * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1215  * and reserve space for them, because it probably doesn't.
1217  * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1218  * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1219  * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1220  * and then reattach (or show) them back.
1222  * @static
1223  * @param {HTMLElement} el Element to reconsider the scrollbars on
1224  */
1225 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1226         var i, len, nodes = [];
1227         // Detach all children
1228         while ( el.firstChild ) {
1229                 nodes.push( el.firstChild );
1230                 el.removeChild( el.firstChild );
1231         }
1232         // Force reflow
1233         void el.offsetHeight;
1234         // Reattach all children
1235         for ( i = 0, len = nodes.length; i < len; i++ ) {
1236                 el.appendChild( nodes[ i ] );
1237         }
1240 /* Methods */
1243  * Get element data.
1245  * @return {Mixed} Element data
1246  */
1247 OO.ui.Element.prototype.getData = function () {
1248         return this.data;
1252  * Set element data.
1254  * @param {Mixed} Element data
1255  * @chainable
1256  */
1257 OO.ui.Element.prototype.setData = function ( data ) {
1258         this.data = data;
1259         return this;
1263  * Check if element supports one or more methods.
1265  * @param {string|string[]} methods Method or list of methods to check
1266  * @return {boolean} All methods are supported
1267  */
1268 OO.ui.Element.prototype.supports = function ( methods ) {
1269         var i, len,
1270                 support = 0;
1272         methods = Array.isArray( methods ) ? methods : [ methods ];
1273         for ( i = 0, len = methods.length; i < len; i++ ) {
1274                 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1275                         support++;
1276                 }
1277         }
1279         return methods.length === support;
1283  * Update the theme-provided classes.
1285  * @localdoc This is called in element mixins and widget classes any time state changes.
1286  *   Updating is debounced, minimizing overhead of changing multiple attributes and
1287  *   guaranteeing that theme updates do not occur within an element's constructor
1288  */
1289 OO.ui.Element.prototype.updateThemeClasses = function () {
1290         if ( !this.updateThemeClassesPending ) {
1291                 this.updateThemeClassesPending = true;
1292                 setTimeout( this.debouncedUpdateThemeClassesHandler );
1293         }
1297  * @private
1298  */
1299 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1300         OO.ui.theme.updateElementClasses( this );
1301         this.updateThemeClassesPending = false;
1305  * Get the HTML tag name.
1307  * Override this method to base the result on instance information.
1309  * @return {string} HTML tag name
1310  */
1311 OO.ui.Element.prototype.getTagName = function () {
1312         return this.constructor.static.tagName;
1316  * Check if the element is attached to the DOM
1317  * @return {boolean} The element is attached to the DOM
1318  */
1319 OO.ui.Element.prototype.isElementAttached = function () {
1320         return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1324  * Get the DOM document.
1326  * @return {HTMLDocument} Document object
1327  */
1328 OO.ui.Element.prototype.getElementDocument = function () {
1329         // Don't cache this in other ways either because subclasses could can change this.$element
1330         return OO.ui.Element.static.getDocument( this.$element );
1334  * Get the DOM window.
1336  * @return {Window} Window object
1337  */
1338 OO.ui.Element.prototype.getElementWindow = function () {
1339         return OO.ui.Element.static.getWindow( this.$element );
1343  * Get closest scrollable container.
1344  */
1345 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1346         return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1350  * Get group element is in.
1352  * @return {OO.ui.GroupElement|null} Group element, null if none
1353  */
1354 OO.ui.Element.prototype.getElementGroup = function () {
1355         return this.elementGroup;
1359  * Set group element is in.
1361  * @param {OO.ui.GroupElement|null} group Group element, null if none
1362  * @chainable
1363  */
1364 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1365         this.elementGroup = group;
1366         return this;
1370  * Scroll element into view.
1372  * @param {Object} [config] Configuration options
1373  */
1374 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1375         return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1379  * Container for elements.
1381  * @abstract
1382  * @class
1383  * @extends OO.ui.Element
1384  * @mixins OO.EventEmitter
1386  * @constructor
1387  * @param {Object} [config] Configuration options
1388  */
1389 OO.ui.Layout = function OoUiLayout( config ) {
1390         // Configuration initialization
1391         config = config || {};
1393         // Parent constructor
1394         OO.ui.Layout.super.call( this, config );
1396         // Mixin constructors
1397         OO.EventEmitter.call( this );
1399         // Initialization
1400         this.$element.addClass( 'oo-ui-layout' );
1403 /* Setup */
1405 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1406 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1409  * Widgets are compositions of one or more OOjs UI elements that users can both view
1410  * and interact with. All widgets can be configured and modified via a standard API,
1411  * and their state can change dynamically according to a model.
1413  * @abstract
1414  * @class
1415  * @extends OO.ui.Element
1416  * @mixins OO.EventEmitter
1418  * @constructor
1419  * @param {Object} [config] Configuration options
1420  * @cfg {boolean} [disabled=false] Disable
1421  */
1422 OO.ui.Widget = function OoUiWidget( config ) {
1423         // Initialize config
1424         config = $.extend( { disabled: false }, config );
1426         // Parent constructor
1427         OO.ui.Widget.super.call( this, config );
1429         // Mixin constructors
1430         OO.EventEmitter.call( this );
1432         // Properties
1433         this.visible = true;
1434         this.disabled = null;
1435         this.wasDisabled = null;
1437         // Initialization
1438         this.$element.addClass( 'oo-ui-widget' );
1439         this.setDisabled( !!config.disabled );
1442 /* Setup */
1444 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1445 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1447 /* Events */
1450  * @event disable
1451  * @param {boolean} disabled Widget is disabled
1452  */
1455  * @event toggle
1456  * @param {boolean} visible Widget is visible
1457  */
1459 /* Methods */
1462  * Check if the widget is disabled.
1464  * @return {boolean} Button is disabled
1465  */
1466 OO.ui.Widget.prototype.isDisabled = function () {
1467         return this.disabled;
1471  * Check if widget is visible.
1473  * @return {boolean} Widget is visible
1474  */
1475 OO.ui.Widget.prototype.isVisible = function () {
1476         return this.visible;
1480  * Set the disabled state of the widget.
1482  * This should probably change the widgets' appearance and prevent it from being used.
1484  * @param {boolean} disabled Disable widget
1485  * @chainable
1486  */
1487 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1488         var isDisabled;
1490         this.disabled = !!disabled;
1491         isDisabled = this.isDisabled();
1492         if ( isDisabled !== this.wasDisabled ) {
1493                 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1494                 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1495                 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1496                 this.emit( 'disable', isDisabled );
1497                 this.updateThemeClasses();
1498         }
1499         this.wasDisabled = isDisabled;
1501         return this;
1505  * Toggle visibility of widget.
1507  * @param {boolean} [show] Make widget visible, omit to toggle visibility
1508  * @fires visible
1509  * @chainable
1510  */
1511 OO.ui.Widget.prototype.toggle = function ( show ) {
1512         show = show === undefined ? !this.visible : !!show;
1514         if ( show !== this.isVisible() ) {
1515                 this.visible = show;
1516                 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1517                 this.emit( 'toggle', show );
1518         }
1520         return this;
1524  * Update the disabled state, in case of changes in parent widget.
1526  * @chainable
1527  */
1528 OO.ui.Widget.prototype.updateDisabled = function () {
1529         this.setDisabled( this.disabled );
1530         return this;
1534  * A window is a container for elements that are in a child frame. They are used with
1535  * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1536  * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1537  * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1538  * the window manager will choose a sensible fallback.
1540  * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1541  * different processes are executed:
1543  * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1544  * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1545  * the window.
1547  * - {@link #getSetupProcess} method is called and its result executed
1548  * - {@link #getReadyProcess} method is called and its result executed
1550  * **opened**: The window is now open
1552  * **closing**: The closing stage begins when the window manager's
1553  * {@link OO.ui.WindowManager#closeWindow closeWindow}
1554  * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1556  * - {@link #getHoldProcess} method is called and its result executed
1557  * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1559  * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1560  * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1561  * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1562  * processing can complete. Always assume window processes are executed asynchronously.
1564  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1566  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1568  * @abstract
1569  * @class
1570  * @extends OO.ui.Element
1571  * @mixins OO.EventEmitter
1573  * @constructor
1574  * @param {Object} [config] Configuration options
1575  * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large`, `larger` or
1576  *  `full`; omit to use #static-size
1577  */
1578 OO.ui.Window = function OoUiWindow( config ) {
1579         // Configuration initialization
1580         config = config || {};
1582         // Parent constructor
1583         OO.ui.Window.super.call( this, config );
1585         // Mixin constructors
1586         OO.EventEmitter.call( this );
1588         // Properties
1589         this.manager = null;
1590         this.size = config.size || this.constructor.static.size;
1591         this.$frame = $( '<div>' );
1592         this.$overlay = $( '<div>' );
1593         this.$content = $( '<div>' );
1595         // Initialization
1596         this.$overlay.addClass( 'oo-ui-window-overlay' );
1597         this.$content
1598                 .addClass( 'oo-ui-window-content' )
1599                 .attr( 'tabIndex', 0 );
1600         this.$frame
1601                 .addClass( 'oo-ui-window-frame' )
1602                 .append( this.$content );
1604         this.$element
1605                 .addClass( 'oo-ui-window' )
1606                 .append( this.$frame, this.$overlay );
1608         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1609         // that reference properties not initialized at that time of parent class construction
1610         // TODO: Find a better way to handle post-constructor setup
1611         this.visible = false;
1612         this.$element.addClass( 'oo-ui-element-hidden' );
1615 /* Setup */
1617 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1618 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1620 /* Static Properties */
1623  * Symbolic name of size.
1625  * Size is used if no size is configured during construction.
1627  * @static
1628  * @inheritable
1629  * @property {string}
1630  */
1631 OO.ui.Window.static.size = 'medium';
1633 /* Methods */
1636  * Handle mouse down events.
1638  * @param {jQuery.Event} e Mouse down event
1639  */
1640 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1641         // Prevent clicking on the click-block from stealing focus
1642         if ( e.target === this.$element[ 0 ] ) {
1643                 return false;
1644         }
1648  * Check if window has been initialized.
1650  * Initialization occurs when a window is added to a manager.
1652  * @return {boolean} Window has been initialized
1653  */
1654 OO.ui.Window.prototype.isInitialized = function () {
1655         return !!this.manager;
1659  * Check if window is visible.
1661  * @return {boolean} Window is visible
1662  */
1663 OO.ui.Window.prototype.isVisible = function () {
1664         return this.visible;
1668  * Check if window is opening.
1670  * This is a wrapper around OO.ui.WindowManager#isOpening.
1672  * @return {boolean} Window is opening
1673  */
1674 OO.ui.Window.prototype.isOpening = function () {
1675         return this.manager.isOpening( this );
1679  * Check if window is closing.
1681  * This is a wrapper around OO.ui.WindowManager#isClosing.
1683  * @return {boolean} Window is closing
1684  */
1685 OO.ui.Window.prototype.isClosing = function () {
1686         return this.manager.isClosing( this );
1690  * Check if window is opened.
1692  * This is a wrapper around OO.ui.WindowManager#isOpened.
1694  * @return {boolean} Window is opened
1695  */
1696 OO.ui.Window.prototype.isOpened = function () {
1697         return this.manager.isOpened( this );
1701  * Get the window manager.
1703  * @return {OO.ui.WindowManager} Manager of window
1704  */
1705 OO.ui.Window.prototype.getManager = function () {
1706         return this.manager;
1710  * Get the window size.
1712  * @return {string} Symbolic size name, e.g. `small`, `medium`, `large`, `larger`, `full`
1713  */
1714 OO.ui.Window.prototype.getSize = function () {
1715         return this.size;
1719  * Disable transitions on window's frame for the duration of the callback function, then enable them
1720  * back.
1722  * @private
1723  * @param {Function} callback Function to call while transitions are disabled
1724  */
1725 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
1726         // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1727         // Disable transitions first, otherwise we'll get values from when the window was animating.
1728         var oldTransition,
1729                 styleObj = this.$frame[ 0 ].style;
1730         oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
1731                 styleObj.MozTransition || styleObj.WebkitTransition;
1732         styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1733                 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
1734         callback();
1735         // Force reflow to make sure the style changes done inside callback really are not transitioned
1736         this.$frame.height();
1737         styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1738                 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
1742  * Get the height of the dialog contents.
1744  * @return {number} Content height
1745  */
1746 OO.ui.Window.prototype.getContentHeight = function () {
1747         var bodyHeight,
1748                 win = this,
1749                 bodyStyleObj = this.$body[ 0 ].style,
1750                 frameStyleObj = this.$frame[ 0 ].style;
1752         // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1753         // Disable transitions first, otherwise we'll get values from when the window was animating.
1754         this.withoutSizeTransitions( function () {
1755                 var oldHeight = frameStyleObj.height,
1756                         oldPosition = bodyStyleObj.position;
1757                 frameStyleObj.height = '1px';
1758                 // Force body to resize to new width
1759                 bodyStyleObj.position = 'relative';
1760                 bodyHeight = win.getBodyHeight();
1761                 frameStyleObj.height = oldHeight;
1762                 bodyStyleObj.position = oldPosition;
1763         } );
1765         return (
1766                 // Add buffer for border
1767                 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1768                 // Use combined heights of children
1769                 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1770         );
1774  * Get the height of the dialog contents.
1776  * When this function is called, the dialog will temporarily have been resized
1777  * to height=1px, so .scrollHeight measurements can be taken accurately.
1779  * @return {number} Height of content
1780  */
1781 OO.ui.Window.prototype.getBodyHeight = function () {
1782         return this.$body[ 0 ].scrollHeight;
1786  * Get the directionality of the frame
1788  * @return {string} Directionality, 'ltr' or 'rtl'
1789  */
1790 OO.ui.Window.prototype.getDir = function () {
1791         return this.dir;
1795  * Get a process for setting up a window for use.
1797  * Each time the window is opened this process will set it up for use in a particular context, based
1798  * on the `data` argument.
1800  * When you override this method, you can add additional setup steps to the process the parent
1801  * method provides using the 'first' and 'next' methods.
1803  * @abstract
1804  * @param {Object} [data] Window opening data
1805  * @return {OO.ui.Process} Setup process
1806  */
1807 OO.ui.Window.prototype.getSetupProcess = function () {
1808         return new OO.ui.Process();
1812  * Get a process for readying a window for use.
1814  * Each time the window is open and setup, this process will ready it up for use in a particular
1815  * context, based on the `data` argument.
1817  * When you override this method, you can add additional setup steps to the process the parent
1818  * method provides using the 'first' and 'next' methods.
1820  * @abstract
1821  * @param {Object} [data] Window opening data
1822  * @return {OO.ui.Process} Setup process
1823  */
1824 OO.ui.Window.prototype.getReadyProcess = function () {
1825         return new OO.ui.Process();
1829  * Get a process for holding a window from use.
1831  * Each time the window is closed, this process will hold it from use in a particular context, based
1832  * on the `data` argument.
1834  * When you override this method, you can add additional setup steps to the process the parent
1835  * method provides using the 'first' and 'next' methods.
1837  * @abstract
1838  * @param {Object} [data] Window closing data
1839  * @return {OO.ui.Process} Hold process
1840  */
1841 OO.ui.Window.prototype.getHoldProcess = function () {
1842         return new OO.ui.Process();
1846  * Get a process for tearing down a window after use.
1848  * Each time the window is closed this process will tear it down and do something with the user's
1849  * interactions within the window, based on the `data` argument.
1851  * When you override this method, you can add additional teardown steps to the process the parent
1852  * method provides using the 'first' and 'next' methods.
1854  * @abstract
1855  * @param {Object} [data] Window closing data
1856  * @return {OO.ui.Process} Teardown process
1857  */
1858 OO.ui.Window.prototype.getTeardownProcess = function () {
1859         return new OO.ui.Process();
1863  * Toggle visibility of window.
1865  * @param {boolean} [show] Make window visible, omit to toggle visibility
1866  * @fires toggle
1867  * @chainable
1868  */
1869 OO.ui.Window.prototype.toggle = function ( show ) {
1870         show = show === undefined ? !this.visible : !!show;
1872         if ( show !== this.isVisible() ) {
1873                 this.visible = show;
1874                 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1875                 this.emit( 'toggle', show );
1876         }
1878         return this;
1882  * Set the window manager.
1884  * This will cause the window to initialize. Calling it more than once will cause an error.
1886  * @param {OO.ui.WindowManager} manager Manager for this window
1887  * @throws {Error} If called more than once
1888  * @chainable
1889  */
1890 OO.ui.Window.prototype.setManager = function ( manager ) {
1891         if ( this.manager ) {
1892                 throw new Error( 'Cannot set window manager, window already has a manager' );
1893         }
1895         this.manager = manager;
1896         this.initialize();
1898         return this;
1902  * Set the window size.
1904  * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1905  * @chainable
1906  */
1907 OO.ui.Window.prototype.setSize = function ( size ) {
1908         this.size = size;
1909         this.updateSize();
1910         return this;
1914  * Update the window size.
1916  * @throws {Error} If not attached to a manager
1917  * @chainable
1918  */
1919 OO.ui.Window.prototype.updateSize = function () {
1920         if ( !this.manager ) {
1921                 throw new Error( 'Cannot update window size, must be attached to a manager' );
1922         }
1924         this.manager.updateWindowSize( this );
1926         return this;
1930  * Set window dimensions.
1932  * Properties are applied to the frame container.
1934  * @param {Object} dim CSS dimension properties
1935  * @param {string|number} [dim.width] Width
1936  * @param {string|number} [dim.minWidth] Minimum width
1937  * @param {string|number} [dim.maxWidth] Maximum width
1938  * @param {string|number} [dim.width] Height, omit to set based on height of contents
1939  * @param {string|number} [dim.minWidth] Minimum height
1940  * @param {string|number} [dim.maxWidth] Maximum height
1941  * @chainable
1942  */
1943 OO.ui.Window.prototype.setDimensions = function ( dim ) {
1944         var height,
1945                 win = this,
1946                 styleObj = this.$frame[ 0 ].style;
1948         // Calculate the height we need to set using the correct width
1949         if ( dim.height === undefined ) {
1950                 this.withoutSizeTransitions( function () {
1951                         var oldWidth = styleObj.width;
1952                         win.$frame.css( 'width', dim.width || '' );
1953                         height = win.getContentHeight();
1954                         styleObj.width = oldWidth;
1955                 } );
1956         } else {
1957                 height = dim.height;
1958         }
1960         this.$frame.css( {
1961                 width: dim.width || '',
1962                 minWidth: dim.minWidth || '',
1963                 maxWidth: dim.maxWidth || '',
1964                 height: height || '',
1965                 minHeight: dim.minHeight || '',
1966                 maxHeight: dim.maxHeight || ''
1967         } );
1969         return this;
1973  * Initialize window contents.
1975  * The first time the window is opened, #initialize is called so that changes to the window that
1976  * will persist between openings can be made. See #getSetupProcess for a way to make changes each
1977  * time the window opens.
1979  * @throws {Error} If not attached to a manager
1980  * @chainable
1981  */
1982 OO.ui.Window.prototype.initialize = function () {
1983         if ( !this.manager ) {
1984                 throw new Error( 'Cannot initialize window, must be attached to a manager' );
1985         }
1987         // Properties
1988         this.$head = $( '<div>' );
1989         this.$body = $( '<div>' );
1990         this.$foot = $( '<div>' );
1991         this.$innerOverlay = $( '<div>' );
1992         this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
1993         this.$document = $( this.getElementDocument() );
1995         // Events
1996         this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
1998         // Initialization
1999         this.$head.addClass( 'oo-ui-window-head' );
2000         this.$body.addClass( 'oo-ui-window-body' );
2001         this.$foot.addClass( 'oo-ui-window-foot' );
2002         this.$innerOverlay.addClass( 'oo-ui-window-inner-overlay' );
2003         this.$content.append( this.$head, this.$body, this.$foot, this.$innerOverlay );
2005         return this;
2009  * Open window.
2011  * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
2012  * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
2014  * @param {Object} [data] Window opening data
2015  * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
2016  *   first argument will be a promise which will be resolved when the window begins closing
2017  * @throws {Error} If not attached to a manager
2018  */
2019 OO.ui.Window.prototype.open = function ( data ) {
2020         if ( !this.manager ) {
2021                 throw new Error( 'Cannot open window, must be attached to a manager' );
2022         }
2024         return this.manager.openWindow( this, data );
2028  * Close window.
2030  * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
2031  * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
2033  * @param {Object} [data] Window closing data
2034  * @return {jQuery.Promise} Promise resolved when window is closed
2035  * @throws {Error} If not attached to a manager
2036  */
2037 OO.ui.Window.prototype.close = function ( data ) {
2038         if ( !this.manager ) {
2039                 throw new Error( 'Cannot close window, must be attached to a manager' );
2040         }
2042         return this.manager.closeWindow( this, data );
2046  * Setup window.
2048  * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2049  * by other systems.
2051  * @param {Object} [data] Window opening data
2052  * @return {jQuery.Promise} Promise resolved when window is setup
2053  */
2054 OO.ui.Window.prototype.setup = function ( data ) {
2055         var win = this,
2056                 deferred = $.Deferred();
2058         this.toggle( true );
2060         this.getSetupProcess( data ).execute().done( function () {
2061                 // Force redraw by asking the browser to measure the elements' widths
2062                 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2063                 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2064                 deferred.resolve();
2065         } );
2067         return deferred.promise();
2071  * Ready window.
2073  * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2074  * by other systems.
2076  * @param {Object} [data] Window opening data
2077  * @return {jQuery.Promise} Promise resolved when window is ready
2078  */
2079 OO.ui.Window.prototype.ready = function ( data ) {
2080         var win = this,
2081                 deferred = $.Deferred();
2083         this.$content.focus();
2084         this.getReadyProcess( data ).execute().done( function () {
2085                 // Force redraw by asking the browser to measure the elements' widths
2086                 win.$element.addClass( 'oo-ui-window-ready' ).width();
2087                 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2088                 deferred.resolve();
2089         } );
2091         return deferred.promise();
2095  * Hold window.
2097  * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2098  * by other systems.
2100  * @param {Object} [data] Window closing data
2101  * @return {jQuery.Promise} Promise resolved when window is held
2102  */
2103 OO.ui.Window.prototype.hold = function ( data ) {
2104         var win = this,
2105                 deferred = $.Deferred();
2107         this.getHoldProcess( data ).execute().done( function () {
2108                 // Get the focused element within the window's content
2109                 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2111                 // Blur the focused element
2112                 if ( $focus.length ) {
2113                         $focus[ 0 ].blur();
2114                 }
2116                 // Force redraw by asking the browser to measure the elements' widths
2117                 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2118                 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2119                 deferred.resolve();
2120         } );
2122         return deferred.promise();
2126  * Teardown window.
2128  * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2129  * by other systems.
2131  * @param {Object} [data] Window closing data
2132  * @return {jQuery.Promise} Promise resolved when window is torn down
2133  */
2134 OO.ui.Window.prototype.teardown = function ( data ) {
2135         var win = this;
2137         return this.getTeardownProcess( data ).execute()
2138                 .done( function () {
2139                         // Force redraw by asking the browser to measure the elements' widths
2140                         win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2141                         win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2142                         win.toggle( false );
2143                 } );
2147  * The Dialog class serves as the base class for the other types of dialogs.
2148  * Unless extended to include controls, the rendered dialog box is a simple window
2149  * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2150  * which opens, closes, and controls the presentation of the window. See the
2151  * [OOjs UI documentation on MediaWiki] [1] for more information.
2153  *     @example
2154  *     // A simple dialog window.
2155  *     function MyDialog( config ) {
2156  *         MyDialog.super.call( this, config );
2157  *     }
2158  *     OO.inheritClass( MyDialog, OO.ui.Dialog );
2159  *     MyDialog.prototype.initialize = function () {
2160  *         MyDialog.super.prototype.initialize.call( this );
2161  *         this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2162  *         this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2163  *         this.$body.append( this.content.$element );
2164  *     };
2165  *     MyDialog.prototype.getBodyHeight = function () {
2166  *         return this.content.$element.outerHeight( true );
2167  *     };
2168  *     var myDialog = new MyDialog( {
2169  *         size: 'medium'
2170  *     } );
2171  *     // Create and append a window manager, which opens and closes the window.
2172  *     var windowManager = new OO.ui.WindowManager();
2173  *     $( 'body' ).append( windowManager.$element );
2174  *     windowManager.addWindows( [ myDialog ] );
2175  *     // Open the window!
2176  *     windowManager.openWindow( myDialog );
2178  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2180  * @abstract
2181  * @class
2182  * @extends OO.ui.Window
2183  * @mixins OO.ui.PendingElement
2185  * @constructor
2186  * @param {Object} [config] Configuration options
2187  */
2188 OO.ui.Dialog = function OoUiDialog( config ) {
2189         // Parent constructor
2190         OO.ui.Dialog.super.call( this, config );
2192         // Mixin constructors
2193         OO.ui.PendingElement.call( this );
2195         // Properties
2196         this.actions = new OO.ui.ActionSet();
2197         this.attachedActions = [];
2198         this.currentAction = null;
2199         this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2201         // Events
2202         this.actions.connect( this, {
2203                 click: 'onActionClick',
2204                 resize: 'onActionResize',
2205                 change: 'onActionsChange'
2206         } );
2208         // Initialization
2209         this.$element
2210                 .addClass( 'oo-ui-dialog' )
2211                 .attr( 'role', 'dialog' );
2214 /* Setup */
2216 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2217 OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement );
2219 /* Static Properties */
2222  * Symbolic name of dialog.
2224  * @abstract
2225  * @static
2226  * @inheritable
2227  * @property {string}
2228  */
2229 OO.ui.Dialog.static.name = '';
2232  * Dialog title.
2234  * @abstract
2235  * @static
2236  * @inheritable
2237  * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
2238  */
2239 OO.ui.Dialog.static.title = '';
2242  * List of OO.ui.ActionWidget configuration options.
2244  * @static
2245  * inheritable
2246  * @property {Object[]}
2247  */
2248 OO.ui.Dialog.static.actions = [];
2251  * Close dialog when the escape key is pressed.
2253  * @static
2254  * @abstract
2255  * @inheritable
2256  * @property {boolean}
2257  */
2258 OO.ui.Dialog.static.escapable = true;
2260 /* Methods */
2263  * Handle frame document key down events.
2265  * @param {jQuery.Event} e Key down event
2266  */
2267 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2268         if ( e.which === OO.ui.Keys.ESCAPE ) {
2269                 this.close();
2270                 e.preventDefault();
2271                 e.stopPropagation();
2272         }
2276  * Handle action resized events.
2278  * @param {OO.ui.ActionWidget} action Action that was resized
2279  */
2280 OO.ui.Dialog.prototype.onActionResize = function () {
2281         // Override in subclass
2285  * Handle action click events.
2287  * @param {OO.ui.ActionWidget} action Action that was clicked
2288  */
2289 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2290         if ( !this.isPending() ) {
2291                 this.currentAction = action;
2292                 this.executeAction( action.getAction() );
2293         }
2297  * Handle actions change event.
2298  */
2299 OO.ui.Dialog.prototype.onActionsChange = function () {
2300         this.detachActions();
2301         if ( !this.isClosing() ) {
2302                 this.attachActions();
2303         }
2307  * Get set of actions.
2309  * @return {OO.ui.ActionSet}
2310  */
2311 OO.ui.Dialog.prototype.getActions = function () {
2312         return this.actions;
2316  * Get a process for taking action.
2318  * When you override this method, you can add additional accept steps to the process the parent
2319  * method provides using the 'first' and 'next' methods.
2321  * @abstract
2322  * @param {string} [action] Symbolic name of action
2323  * @return {OO.ui.Process} Action process
2324  */
2325 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2326         return new OO.ui.Process()
2327                 .next( function () {
2328                         if ( !action ) {
2329                                 // An empty action always closes the dialog without data, which should always be
2330                                 // safe and make no changes
2331                                 this.close();
2332                         }
2333                 }, this );
2337  * @inheritdoc
2339  * @param {Object} [data] Dialog opening data
2340  * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2341  * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2342  *   action item, omit to use #static-actions
2343  */
2344 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2345         data = data || {};
2347         // Parent method
2348         return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2349                 .next( function () {
2350                         var i, len,
2351                                 items = [],
2352                                 config = this.constructor.static,
2353                                 actions = data.actions !== undefined ? data.actions : config.actions;
2355                         this.title.setLabel(
2356                                 data.title !== undefined ? data.title : this.constructor.static.title
2357                         );
2358                         for ( i = 0, len = actions.length; i < len; i++ ) {
2359                                 items.push(
2360                                         new OO.ui.ActionWidget( actions[ i ] )
2361                                 );
2362                         }
2363                         this.actions.add( items );
2365                         if ( this.constructor.static.escapable ) {
2366                                 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2367                         }
2368                 }, this );
2372  * @inheritdoc
2373  */
2374 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2375         // Parent method
2376         return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2377                 .first( function () {
2378                         if ( this.constructor.static.escapable ) {
2379                                 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2380                         }
2382                         this.actions.clear();
2383                         this.currentAction = null;
2384                 }, this );
2388  * @inheritdoc
2389  */
2390 OO.ui.Dialog.prototype.initialize = function () {
2391         // Parent method
2392         OO.ui.Dialog.super.prototype.initialize.call( this );
2394         // Properties
2395         this.title = new OO.ui.LabelWidget();
2397         // Initialization
2398         this.$content.addClass( 'oo-ui-dialog-content' );
2399         this.setPendingElement( this.$head );
2403  * Attach action actions.
2404  */
2405 OO.ui.Dialog.prototype.attachActions = function () {
2406         // Remember the list of potentially attached actions
2407         this.attachedActions = this.actions.get();
2411  * Detach action actions.
2413  * @chainable
2414  */
2415 OO.ui.Dialog.prototype.detachActions = function () {
2416         var i, len;
2418         // Detach all actions that may have been previously attached
2419         for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2420                 this.attachedActions[ i ].$element.detach();
2421         }
2422         this.attachedActions = [];
2426  * Execute an action.
2428  * @param {string} action Symbolic name of action to execute
2429  * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2430  */
2431 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2432         this.pushPending();
2433         return this.getActionProcess( action ).execute()
2434                 .always( this.popPending.bind( this ) );
2438  * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2439  * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2440  * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2441  * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2442  * pertinent data and reused.
2444  * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2445  * `opened`, and `closing`, which represent the primary stages of the cycle:
2447  * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2448  * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2450  * - an `opening` event is emitted with an `opening` promise
2451  * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2452  *   the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2453  *   window and its result executed
2454  * - a `setup` progress notification is emitted from the `opening` promise
2455  * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2456  *   the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2457  *   window and its result executed
2458  * - a `ready` progress notification is emitted from the `opening` promise
2459  * - the `opening` promise is resolved with an `opened` promise
2461  * **Opened**: the window is now open.
2463  * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2464  * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2465  * to close the window.
2467  * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2468  * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2469  *   the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2470  *   window and its result executed
2471  * - a `hold` progress notification is emitted from the `closing` promise
2472  * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2473  *   the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2474  *   window and its result executed
2475  * - a `teardown` progress notification is emitted from the `closing` promise
2476  * - the `closing` promise is resolved. The window is now closed
2478  * See the [OOjs UI documentation on MediaWiki][1] for more information.
2480  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2482  * @class
2483  * @extends OO.ui.Element
2484  * @mixins OO.EventEmitter
2486  * @constructor
2487  * @param {Object} [config] Configuration options
2488  * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2489  * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2490  */
2491 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2492         // Configuration initialization
2493         config = config || {};
2495         // Parent constructor
2496         OO.ui.WindowManager.super.call( this, config );
2498         // Mixin constructors
2499         OO.EventEmitter.call( this );
2501         // Properties
2502         this.factory = config.factory;
2503         this.modal = config.modal === undefined || !!config.modal;
2504         this.windows = {};
2505         this.opening = null;
2506         this.opened = null;
2507         this.closing = null;
2508         this.preparingToOpen = null;
2509         this.preparingToClose = null;
2510         this.currentWindow = null;
2511         this.$ariaHidden = null;
2512         this.onWindowResizeTimeout = null;
2513         this.onWindowResizeHandler = this.onWindowResize.bind( this );
2514         this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2516         // Initialization
2517         this.$element
2518                 .addClass( 'oo-ui-windowManager' )
2519                 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2522 /* Setup */
2524 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2525 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2527 /* Events */
2530  * Window is opening.
2532  * Fired when the window begins to be opened.
2534  * @event opening
2535  * @param {OO.ui.Window} win Window that's being opened
2536  * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2537  *   resolved the first argument will be a promise which will be resolved when the window begins
2538  *   closing, the second argument will be the opening data; progress notifications will be fired on
2539  *   the promise for `setup` and `ready` when those processes are completed respectively.
2540  * @param {Object} data Window opening data
2541  */
2544  * Window is closing.
2546  * Fired when the window begins to be closed.
2548  * @event closing
2549  * @param {OO.ui.Window} win Window that's being closed
2550  * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
2551  *   is resolved the first argument will be a the closing data; progress notifications will be fired
2552  *   on the promise for `hold` and `teardown` when those processes are completed respectively.
2553  * @param {Object} data Window closing data
2554  */
2557  * Window was resized.
2559  * @event resize
2560  * @param {OO.ui.Window} win Window that was resized
2561  */
2563 /* Static Properties */
2566  * Map of symbolic size names and CSS properties.
2568  * @static
2569  * @inheritable
2570  * @property {Object}
2571  */
2572 OO.ui.WindowManager.static.sizes = {
2573         small: {
2574                 width: 300
2575         },
2576         medium: {
2577                 width: 500
2578         },
2579         large: {
2580                 width: 700
2581         },
2582         larger: {
2583                 width: 900
2584         },
2585         full: {
2586                 // These can be non-numeric because they are never used in calculations
2587                 width: '100%',
2588                 height: '100%'
2589         }
2593  * Symbolic name of default size.
2595  * Default size is used if the window's requested size is not recognized.
2597  * @static
2598  * @inheritable
2599  * @property {string}
2600  */
2601 OO.ui.WindowManager.static.defaultSize = 'medium';
2603 /* Methods */
2606  * Handle window resize events.
2608  * @param {jQuery.Event} e Window resize event
2609  */
2610 OO.ui.WindowManager.prototype.onWindowResize = function () {
2611         clearTimeout( this.onWindowResizeTimeout );
2612         this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
2616  * Handle window resize events.
2618  * @param {jQuery.Event} e Window resize event
2619  */
2620 OO.ui.WindowManager.prototype.afterWindowResize = function () {
2621         if ( this.currentWindow ) {
2622                 this.updateWindowSize( this.currentWindow );
2623         }
2627  * Check if window is opening.
2629  * @return {boolean} Window is opening
2630  */
2631 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
2632         return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
2636  * Check if window is closing.
2638  * @return {boolean} Window is closing
2639  */
2640 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
2641         return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
2645  * Check if window is opened.
2647  * @return {boolean} Window is opened
2648  */
2649 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
2650         return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
2654  * Check if a window is being managed.
2656  * @param {OO.ui.Window} win Window to check
2657  * @return {boolean} Window is being managed
2658  */
2659 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
2660         var name;
2662         for ( name in this.windows ) {
2663                 if ( this.windows[ name ] === win ) {
2664                         return true;
2665                 }
2666         }
2668         return false;
2672  * Get the number of milliseconds to wait between beginning opening and executing setup process.
2674  * @param {OO.ui.Window} win Window being opened
2675  * @param {Object} [data] Window opening data
2676  * @return {number} Milliseconds to wait
2677  */
2678 OO.ui.WindowManager.prototype.getSetupDelay = function () {
2679         return 0;
2683  * Get the number of milliseconds to wait between finishing setup and executing ready process.
2685  * @param {OO.ui.Window} win Window being opened
2686  * @param {Object} [data] Window opening data
2687  * @return {number} Milliseconds to wait
2688  */
2689 OO.ui.WindowManager.prototype.getReadyDelay = function () {
2690         return 0;
2694  * Get the number of milliseconds to wait between beginning closing and executing hold process.
2696  * @param {OO.ui.Window} win Window being closed
2697  * @param {Object} [data] Window closing data
2698  * @return {number} Milliseconds to wait
2699  */
2700 OO.ui.WindowManager.prototype.getHoldDelay = function () {
2701         return 0;
2705  * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2707  * @param {OO.ui.Window} win Window being closed
2708  * @param {Object} [data] Window closing data
2709  * @return {number} Milliseconds to wait
2710  */
2711 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
2712         return this.modal ? 250 : 0;
2716  * Get managed window by symbolic name.
2718  * If window is not yet instantiated, it will be instantiated and added automatically.
2720  * @param {string} name Symbolic window name
2721  * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2722  * @throws {Error} If the symbolic name is unrecognized by the factory
2723  * @throws {Error} If the symbolic name unrecognized as a managed window
2724  */
2725 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
2726         var deferred = $.Deferred(),
2727                 win = this.windows[ name ];
2729         if ( !( win instanceof OO.ui.Window ) ) {
2730                 if ( this.factory ) {
2731                         if ( !this.factory.lookup( name ) ) {
2732                                 deferred.reject( new OO.ui.Error(
2733                                         'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2734                                 ) );
2735                         } else {
2736                                 win = this.factory.create( name, this );
2737                                 this.addWindows( [ win ] );
2738                                 deferred.resolve( win );
2739                         }
2740                 } else {
2741                         deferred.reject( new OO.ui.Error(
2742                                 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2743                         ) );
2744                 }
2745         } else {
2746                 deferred.resolve( win );
2747         }
2749         return deferred.promise();
2753  * Get current window.
2755  * @return {OO.ui.Window|null} Currently opening/opened/closing window
2756  */
2757 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
2758         return this.currentWindow;
2762  * Open a window.
2764  * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2765  * @param {Object} [data] Window opening data
2766  * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2767  *   for more details about the `opening` promise
2768  * @fires opening
2769  */
2770 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
2771         var manager = this,
2772                 opening = $.Deferred();
2774         // Argument handling
2775         if ( typeof win === 'string' ) {
2776                 return this.getWindow( win ).then( function ( win ) {
2777                         return manager.openWindow( win, data );
2778                 } );
2779         }
2781         // Error handling
2782         if ( !this.hasWindow( win ) ) {
2783                 opening.reject( new OO.ui.Error(
2784                         'Cannot open window: window is not attached to manager'
2785                 ) );
2786         } else if ( this.preparingToOpen || this.opening || this.opened ) {
2787                 opening.reject( new OO.ui.Error(
2788                         'Cannot open window: another window is opening or open'
2789                 ) );
2790         }
2792         // Window opening
2793         if ( opening.state() !== 'rejected' ) {
2794                 // If a window is currently closing, wait for it to complete
2795                 this.preparingToOpen = $.when( this.closing );
2796                 // Ensure handlers get called after preparingToOpen is set
2797                 this.preparingToOpen.done( function () {
2798                         if ( manager.modal ) {
2799                                 manager.toggleGlobalEvents( true );
2800                                 manager.toggleAriaIsolation( true );
2801                         }
2802                         manager.currentWindow = win;
2803                         manager.opening = opening;
2804                         manager.preparingToOpen = null;
2805                         manager.emit( 'opening', win, opening, data );
2806                         setTimeout( function () {
2807                                 win.setup( data ).then( function () {
2808                                         manager.updateWindowSize( win );
2809                                         manager.opening.notify( { state: 'setup' } );
2810                                         setTimeout( function () {
2811                                                 win.ready( data ).then( function () {
2812                                                         manager.opening.notify( { state: 'ready' } );
2813                                                         manager.opening = null;
2814                                                         manager.opened = $.Deferred();
2815                                                         opening.resolve( manager.opened.promise(), data );
2816                                                 } );
2817                                         }, manager.getReadyDelay() );
2818                                 } );
2819                         }, manager.getSetupDelay() );
2820                 } );
2821         }
2823         return opening.promise();
2827  * Close a window.
2829  * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
2830  * @param {Object} [data] Window closing data
2831  * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
2832  *   for more details about the `closing` promise
2833  * @throws {Error} If no window by that name is being managed
2834  * @fires closing
2835  */
2836 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
2837         var manager = this,
2838                 closing = $.Deferred(),
2839                 opened;
2841         // Argument handling
2842         if ( typeof win === 'string' ) {
2843                 win = this.windows[ win ];
2844         } else if ( !this.hasWindow( win ) ) {
2845                 win = null;
2846         }
2848         // Error handling
2849         if ( !win ) {
2850                 closing.reject( new OO.ui.Error(
2851                         'Cannot close window: window is not attached to manager'
2852                 ) );
2853         } else if ( win !== this.currentWindow ) {
2854                 closing.reject( new OO.ui.Error(
2855                         'Cannot close window: window already closed with different data'
2856                 ) );
2857         } else if ( this.preparingToClose || this.closing ) {
2858                 closing.reject( new OO.ui.Error(
2859                         'Cannot close window: window already closing with different data'
2860                 ) );
2861         }
2863         // Window closing
2864         if ( closing.state() !== 'rejected' ) {
2865                 // If the window is currently opening, close it when it's done
2866                 this.preparingToClose = $.when( this.opening );
2867                 // Ensure handlers get called after preparingToClose is set
2868                 this.preparingToClose.done( function () {
2869                         manager.closing = closing;
2870                         manager.preparingToClose = null;
2871                         manager.emit( 'closing', win, closing, data );
2872                         opened = manager.opened;
2873                         manager.opened = null;
2874                         opened.resolve( closing.promise(), data );
2875                         setTimeout( function () {
2876                                 win.hold( data ).then( function () {
2877                                         closing.notify( { state: 'hold' } );
2878                                         setTimeout( function () {
2879                                                 win.teardown( data ).then( function () {
2880                                                         closing.notify( { state: 'teardown' } );
2881                                                         if ( manager.modal ) {
2882                                                                 manager.toggleGlobalEvents( false );
2883                                                                 manager.toggleAriaIsolation( false );
2884                                                         }
2885                                                         manager.closing = null;
2886                                                         manager.currentWindow = null;
2887                                                         closing.resolve( data );
2888                                                 } );
2889                                         }, manager.getTeardownDelay() );
2890                                 } );
2891                         }, manager.getHoldDelay() );
2892                 } );
2893         }
2895         return closing.promise();
2899  * Add windows.
2901  * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
2902  * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
2903  *   a statically configured symbolic name
2904  */
2905 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
2906         var i, len, win, name, list;
2908         if ( Array.isArray( windows ) ) {
2909                 // Convert to map of windows by looking up symbolic names from static configuration
2910                 list = {};
2911                 for ( i = 0, len = windows.length; i < len; i++ ) {
2912                         name = windows[ i ].constructor.static.name;
2913                         if ( typeof name !== 'string' ) {
2914                                 throw new Error( 'Cannot add window' );
2915                         }
2916                         list[ name ] = windows[ i ];
2917                 }
2918         } else if ( $.isPlainObject( windows ) ) {
2919                 list = windows;
2920         }
2922         // Add windows
2923         for ( name in list ) {
2924                 win = list[ name ];
2925                 this.windows[ name ] = win.toggle( false );
2926                 this.$element.append( win.$element );
2927                 win.setManager( this );
2928         }
2932  * Remove windows.
2934  * Windows will be closed before they are removed.
2936  * @param {string[]} names Symbolic names of windows to remove
2937  * @return {jQuery.Promise} Promise resolved when window is closed and removed
2938  * @throws {Error} If windows being removed are not being managed
2939  */
2940 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
2941         var i, len, win, name, cleanupWindow,
2942                 manager = this,
2943                 promises = [],
2944                 cleanup = function ( name, win ) {
2945                         delete manager.windows[ name ];
2946                         win.$element.detach();
2947                 };
2949         for ( i = 0, len = names.length; i < len; i++ ) {
2950                 name = names[ i ];
2951                 win = this.windows[ name ];
2952                 if ( !win ) {
2953                         throw new Error( 'Cannot remove window' );
2954                 }
2955                 cleanupWindow = cleanup.bind( null, name, win );
2956                 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
2957         }
2959         return $.when.apply( $, promises );
2963  * Remove all windows.
2965  * Windows will be closed before they are removed.
2967  * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
2968  */
2969 OO.ui.WindowManager.prototype.clearWindows = function () {
2970         return this.removeWindows( Object.keys( this.windows ) );
2974  * Set dialog size.
2976  * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
2978  * @chainable
2979  */
2980 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
2981         // Bypass for non-current, and thus invisible, windows
2982         if ( win !== this.currentWindow ) {
2983                 return;
2984         }
2986         var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
2987                 sizes = this.constructor.static.sizes,
2988                 size = win.getSize();
2990         if ( !sizes[ size ] ) {
2991                 size = this.constructor.static.defaultSize;
2992         }
2993         if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
2994                 size = 'full';
2995         }
2997         this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
2998         this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
2999         win.setDimensions( sizes[ size ] );
3001         this.emit( 'resize', win );
3003         return this;
3007  * Bind or unbind global events for scrolling.
3009  * @param {boolean} [on] Bind global events
3010  * @chainable
3011  */
3012 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3013         on = on === undefined ? !!this.globalEvents : !!on;
3015         if ( on ) {
3016                 if ( !this.globalEvents ) {
3017                         $( this.getElementWindow() ).on( {
3018                                 // Start listening for top-level window dimension changes
3019                                 'orientationchange resize': this.onWindowResizeHandler
3020                         } );
3021                         $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
3022                         this.globalEvents = true;
3023                 }
3024         } else if ( this.globalEvents ) {
3025                 $( this.getElementWindow() ).off( {
3026                         // Stop listening for top-level window dimension changes
3027                         'orientationchange resize': this.onWindowResizeHandler
3028                 } );
3029                 $( this.getElementDocument().body ).css( 'overflow', '' );
3030                 this.globalEvents = false;
3031         }
3033         return this;
3037  * Toggle screen reader visibility of content other than the window manager.
3039  * @param {boolean} [isolate] Make only the window manager visible to screen readers
3040  * @chainable
3041  */
3042 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3043         isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3045         if ( isolate ) {
3046                 if ( !this.$ariaHidden ) {
3047                         // Hide everything other than the window manager from screen readers
3048                         this.$ariaHidden = $( 'body' )
3049                                 .children()
3050                                 .not( this.$element.parentsUntil( 'body' ).last() )
3051                                 .attr( 'aria-hidden', '' );
3052                 }
3053         } else if ( this.$ariaHidden ) {
3054                 // Restore screen reader visibility
3055                 this.$ariaHidden.removeAttr( 'aria-hidden' );
3056                 this.$ariaHidden = null;
3057         }
3059         return this;
3063  * Destroy window manager.
3064  */
3065 OO.ui.WindowManager.prototype.destroy = function () {
3066         this.toggleGlobalEvents( false );
3067         this.toggleAriaIsolation( false );
3068         this.clearWindows();
3069         this.$element.remove();
3073  * @class
3075  * @constructor
3076  * @param {string|jQuery} message Description of error
3077  * @param {Object} [config] Configuration options
3078  * @cfg {boolean} [recoverable=true] Error is recoverable
3079  * @cfg {boolean} [warning=false] Whether this error is a warning or not.
3080  */
3081 OO.ui.Error = function OoUiElement( message, config ) {
3082         // Configuration initialization
3083         config = config || {};
3085         // Properties
3086         this.message = message instanceof jQuery ? message : String( message );
3087         this.recoverable = config.recoverable === undefined || !!config.recoverable;
3088         this.warning = !!config.warning;
3091 /* Setup */
3093 OO.initClass( OO.ui.Error );
3095 /* Methods */
3098  * Check if error can be recovered from.
3100  * @return {boolean} Error is recoverable
3101  */
3102 OO.ui.Error.prototype.isRecoverable = function () {
3103         return this.recoverable;
3107  * Check if the error is a warning
3109  * @return {boolean} Error is warning
3110  */
3111 OO.ui.Error.prototype.isWarning = function () {
3112         return this.warning;
3116  * Get error message as DOM nodes.
3118  * @return {jQuery} Error message in DOM nodes
3119  */
3120 OO.ui.Error.prototype.getMessage = function () {
3121         return this.message instanceof jQuery ?
3122                 this.message.clone() :
3123                 $( '<div>' ).text( this.message ).contents();
3127  * Get error message as text.
3129  * @return {string} Error message
3130  */
3131 OO.ui.Error.prototype.getMessageText = function () {
3132         return this.message instanceof jQuery ? this.message.text() : this.message;
3136  * A list of functions, called in sequence.
3138  * If a function added to a process returns boolean false the process will stop; if it returns an
3139  * object with a `promise` method the process will use the promise to either continue to the next
3140  * step when the promise is resolved or stop when the promise is rejected.
3142  * @class
3144  * @constructor
3145  * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3146  *   call, see #createStep for more information
3147  * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3148  *   or a promise
3149  * @return {Object} Step object, with `callback` and `context` properties
3150  */
3151 OO.ui.Process = function ( step, context ) {
3152         // Properties
3153         this.steps = [];
3155         // Initialization
3156         if ( step !== undefined ) {
3157                 this.next( step, context );
3158         }
3161 /* Setup */
3163 OO.initClass( OO.ui.Process );
3165 /* Methods */
3168  * Start the process.
3170  * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3171  *   any of the steps return boolean false or a promise which gets rejected; upon stopping the
3172  *   process, the remaining steps will not be taken
3173  */
3174 OO.ui.Process.prototype.execute = function () {
3175         var i, len, promise;
3177         /**
3178          * Continue execution.
3179          *
3180          * @ignore
3181          * @param {Array} step A function and the context it should be called in
3182          * @return {Function} Function that continues the process
3183          */
3184         function proceed( step ) {
3185                 return function () {
3186                         // Execute step in the correct context
3187                         var deferred,
3188                                 result = step.callback.call( step.context );
3190                         if ( result === false ) {
3191                                 // Use rejected promise for boolean false results
3192                                 return $.Deferred().reject( [] ).promise();
3193                         }
3194                         if ( typeof result === 'number' ) {
3195                                 if ( result < 0 ) {
3196                                         throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3197                                 }
3198                                 // Use a delayed promise for numbers, expecting them to be in milliseconds
3199                                 deferred = $.Deferred();
3200                                 setTimeout( deferred.resolve, result );
3201                                 return deferred.promise();
3202                         }
3203                         if ( result instanceof OO.ui.Error ) {
3204                                 // Use rejected promise for error
3205                                 return $.Deferred().reject( [ result ] ).promise();
3206                         }
3207                         if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3208                                 // Use rejected promise for list of errors
3209                                 return $.Deferred().reject( result ).promise();
3210                         }
3211                         // Duck-type the object to see if it can produce a promise
3212                         if ( result && $.isFunction( result.promise ) ) {
3213                                 // Use a promise generated from the result
3214                                 return result.promise();
3215                         }
3216                         // Use resolved promise for other results
3217                         return $.Deferred().resolve().promise();
3218                 };
3219         }
3221         if ( this.steps.length ) {
3222                 // Generate a chain reaction of promises
3223                 promise = proceed( this.steps[ 0 ] )();
3224                 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3225                         promise = promise.then( proceed( this.steps[ i ] ) );
3226                 }
3227         } else {
3228                 promise = $.Deferred().resolve().promise();
3229         }
3231         return promise;
3235  * Create a process step.
3237  * @private
3238  * @param {number|jQuery.Promise|Function} step
3240  * - Number of milliseconds to wait; or
3241  * - Promise to wait to be resolved; or
3242  * - Function to execute
3243  *   - If it returns boolean false the process will stop
3244  *   - If it returns an object with a `promise` method the process will use the promise to either
3245  *     continue to the next step when the promise is resolved or stop when the promise is rejected
3246  *   - If it returns a number, the process will wait for that number of milliseconds before
3247  *     proceeding
3248  * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3249  *   or a promise
3250  * @return {Object} Step object, with `callback` and `context` properties
3251  */
3252 OO.ui.Process.prototype.createStep = function ( step, context ) {
3253         if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3254                 return {
3255                         callback: function () {
3256                                 return step;
3257                         },
3258                         context: null
3259                 };
3260         }
3261         if ( $.isFunction( step ) ) {
3262                 return {
3263                         callback: step,
3264                         context: context
3265                 };
3266         }
3267         throw new Error( 'Cannot create process step: number, promise or function expected' );
3271  * Add step to the beginning of the process.
3273  * @inheritdoc #createStep
3274  * @return {OO.ui.Process} this
3275  * @chainable
3276  */
3277 OO.ui.Process.prototype.first = function ( step, context ) {
3278         this.steps.unshift( this.createStep( step, context ) );
3279         return this;
3283  * Add step to the end of the process.
3285  * @inheritdoc #createStep
3286  * @return {OO.ui.Process} this
3287  * @chainable
3288  */
3289 OO.ui.Process.prototype.next = function ( step, context ) {
3290         this.steps.push( this.createStep( step, context ) );
3291         return this;
3295  * Factory for tools.
3297  * @class
3298  * @extends OO.Factory
3299  * @constructor
3300  */
3301 OO.ui.ToolFactory = function OoUiToolFactory() {
3302         // Parent constructor
3303         OO.ui.ToolFactory.super.call( this );
3306 /* Setup */
3308 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3310 /* Methods */
3313  * Get tools from the factory
3315  * @param {Array} include Included tools
3316  * @param {Array} exclude Excluded tools
3317  * @param {Array} promote Promoted tools
3318  * @param {Array} demote Demoted tools
3319  * @return {string[]} List of tools
3320  */
3321 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3322         var i, len, included, promoted, demoted,
3323                 auto = [],
3324                 used = {};
3326         // Collect included and not excluded tools
3327         included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3329         // Promotion
3330         promoted = this.extract( promote, used );
3331         demoted = this.extract( demote, used );
3333         // Auto
3334         for ( i = 0, len = included.length; i < len; i++ ) {
3335                 if ( !used[ included[ i ] ] ) {
3336                         auto.push( included[ i ] );
3337                 }
3338         }
3340         return promoted.concat( auto ).concat( demoted );
3344  * Get a flat list of names from a list of names or groups.
3346  * Tools can be specified in the following ways:
3348  * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3349  * - All tools in a group: `{ group: 'group-name' }`
3350  * - All tools: `'*'`
3352  * @private
3353  * @param {Array|string} collection List of tools
3354  * @param {Object} [used] Object with names that should be skipped as properties; extracted
3355  *  names will be added as properties
3356  * @return {string[]} List of extracted names
3357  */
3358 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3359         var i, len, item, name, tool,
3360                 names = [];
3362         if ( collection === '*' ) {
3363                 for ( name in this.registry ) {
3364                         tool = this.registry[ name ];
3365                         if (
3366                                 // Only add tools by group name when auto-add is enabled
3367                                 tool.static.autoAddToCatchall &&
3368                                 // Exclude already used tools
3369                                 ( !used || !used[ name ] )
3370                         ) {
3371                                 names.push( name );
3372                                 if ( used ) {
3373                                         used[ name ] = true;
3374                                 }
3375                         }
3376                 }
3377         } else if ( Array.isArray( collection ) ) {
3378                 for ( i = 0, len = collection.length; i < len; i++ ) {
3379                         item = collection[ i ];
3380                         // Allow plain strings as shorthand for named tools
3381                         if ( typeof item === 'string' ) {
3382                                 item = { name: item };
3383                         }
3384                         if ( OO.isPlainObject( item ) ) {
3385                                 if ( item.group ) {
3386                                         for ( name in this.registry ) {
3387                                                 tool = this.registry[ name ];
3388                                                 if (
3389                                                         // Include tools with matching group
3390                                                         tool.static.group === item.group &&
3391                                                         // Only add tools by group name when auto-add is enabled
3392                                                         tool.static.autoAddToGroup &&
3393                                                         // Exclude already used tools
3394                                                         ( !used || !used[ name ] )
3395                                                 ) {
3396                                                         names.push( name );
3397                                                         if ( used ) {
3398                                                                 used[ name ] = true;
3399                                                         }
3400                                                 }
3401                                         }
3402                                 // Include tools with matching name and exclude already used tools
3403                                 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3404                                         names.push( item.name );
3405                                         if ( used ) {
3406                                                 used[ item.name ] = true;
3407                                         }
3408                                 }
3409                         }
3410                 }
3411         }
3412         return names;
3416  * Factory for tool groups.
3418  * @class
3419  * @extends OO.Factory
3420  * @constructor
3421  */
3422 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3423         // Parent constructor
3424         OO.Factory.call( this );
3426         var i, l,
3427                 defaultClasses = this.constructor.static.getDefaultClasses();
3429         // Register default toolgroups
3430         for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3431                 this.register( defaultClasses[ i ] );
3432         }
3435 /* Setup */
3437 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3439 /* Static Methods */
3442  * Get a default set of classes to be registered on construction
3444  * @return {Function[]} Default classes
3445  */
3446 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3447         return [
3448                 OO.ui.BarToolGroup,
3449                 OO.ui.ListToolGroup,
3450                 OO.ui.MenuToolGroup
3451         ];
3455  * Theme logic.
3457  * @abstract
3458  * @class
3460  * @constructor
3461  * @param {Object} [config] Configuration options
3462  */
3463 OO.ui.Theme = function OoUiTheme( config ) {
3464         // Configuration initialization
3465         config = config || {};
3468 /* Setup */
3470 OO.initClass( OO.ui.Theme );
3472 /* Methods */
3475  * Get a list of classes to be applied to a widget.
3477  * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
3478  * otherwise state transitions will not work properly.
3480  * @param {OO.ui.Element} element Element for which to get classes
3481  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3482  */
3483 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
3484         return { on: [], off: [] };
3488  * Update CSS classes provided by the theme.
3490  * For elements with theme logic hooks, this should be called any time there's a state change.
3492  * @param {OO.ui.Element} element Element for which to update classes
3493  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3494  */
3495 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
3496         var classes = this.getElementClasses( element );
3498         element.$element
3499                 .removeClass( classes.off.join( ' ' ) )
3500                 .addClass( classes.on.join( ' ' ) );
3504  * Element supporting "sequential focus navigation" using the 'tabindex' attribute.
3506  * @abstract
3507  * @class
3509  * @constructor
3510  * @param {Object} [config] Configuration options
3511  * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
3512  * @cfg {number|null} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
3513  *  prevent tab focusing, use null to suppress the `tabindex` attribute.
3514  */
3515 OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
3516         // Configuration initialization
3517         config = $.extend( { tabIndex: 0 }, config );
3519         // Properties
3520         this.$tabIndexed = null;
3521         this.tabIndex = null;
3523         // Events
3524         this.connect( this, { disable: 'onDisable' } );
3526         // Initialization
3527         this.setTabIndex( config.tabIndex );
3528         this.setTabIndexedElement( config.$tabIndexed || this.$element );
3531 /* Setup */
3533 OO.initClass( OO.ui.TabIndexedElement );
3535 /* Methods */
3538  * Set the element with `tabindex` attribute.
3540  * If an element is already set, it will be cleaned up before setting up the new element.
3542  * @param {jQuery} $tabIndexed Element to set tab index on
3543  * @chainable
3544  */
3545 OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
3546         var tabIndex = this.tabIndex;
3547         // Remove attributes from old $tabIndexed
3548         this.setTabIndex( null );
3549         // Force update of new $tabIndexed
3550         this.$tabIndexed = $tabIndexed;
3551         this.tabIndex = tabIndex;
3552         return this.updateTabIndex();
3556  * Set tab index value.
3558  * @param {number|null} tabIndex Tab index value or null for no tab index
3559  * @chainable
3560  */
3561 OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
3562         tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
3564         if ( this.tabIndex !== tabIndex ) {
3565                 this.tabIndex = tabIndex;
3566                 this.updateTabIndex();
3567         }
3569         return this;
3573  * Update the `tabindex` attribute, in case of changes to tab index or
3574  * disabled state.
3576  * @chainable
3577  */
3578 OO.ui.TabIndexedElement.prototype.updateTabIndex = function () {
3579         if ( this.$tabIndexed ) {
3580                 if ( this.tabIndex !== null ) {
3581                         // Do not index over disabled elements
3582                         this.$tabIndexed.attr( {
3583                                 tabindex: this.isDisabled() ? -1 : this.tabIndex,
3584                                 // ChromeVox and NVDA do not seem to inherit this from parent elements
3585                                 'aria-disabled': this.isDisabled().toString()
3586                         } );
3587                 } else {
3588                         this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
3589                 }
3590         }
3591         return this;
3595  * Handle disable events.
3597  * @param {boolean} disabled Element is disabled
3598  */
3599 OO.ui.TabIndexedElement.prototype.onDisable = function () {
3600         this.updateTabIndex();
3604  * Get tab index value.
3606  * @return {number|null} Tab index value
3607  */
3608 OO.ui.TabIndexedElement.prototype.getTabIndex = function () {
3609         return this.tabIndex;
3613  * ButtonElement is often mixed into other classes to generate a button, which is a clickable
3614  * interface element that can be configured with access keys for accessibility.
3615  * See the [OOjs UI documentation on MediaWiki] [1] for examples.
3617  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
3618  * @abstract
3619  * @class
3621  * @constructor
3622  * @param {Object} [config] Configuration options
3623  * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3624  * @cfg {boolean} [framed=true] Render button with a frame
3625  * @cfg {string} [accessKey] Button's access key
3626  */
3627 OO.ui.ButtonElement = function OoUiButtonElement( config ) {
3628         // Configuration initialization
3629         config = config || {};
3631         // Properties
3632         this.$button = config.$button || $( '<a>' );
3633         this.framed = null;
3634         this.accessKey = null;
3635         this.active = false;
3636         this.onMouseUpHandler = this.onMouseUp.bind( this );
3637         this.onMouseDownHandler = this.onMouseDown.bind( this );
3638         this.onKeyDownHandler = this.onKeyDown.bind( this );
3639         this.onKeyUpHandler = this.onKeyUp.bind( this );
3640         this.onClickHandler = this.onClick.bind( this );
3641         this.onKeyPressHandler = this.onKeyPress.bind( this );
3643         // Initialization
3644         this.$element.addClass( 'oo-ui-buttonElement' );
3645         this.toggleFramed( config.framed === undefined || config.framed );
3646         this.setAccessKey( config.accessKey );
3647         this.setButtonElement( this.$button );
3650 /* Setup */
3652 OO.initClass( OO.ui.ButtonElement );
3654 /* Static Properties */
3657  * Cancel mouse down events.
3659  * @static
3660  * @inheritable
3661  * @property {boolean}
3662  */
3663 OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
3665 /* Events */
3668  * @event click
3669  */
3671 /* Methods */
3674  * Set the button element.
3676  * If an element is already set, it will be cleaned up before setting up the new element.
3678  * @param {jQuery} $button Element to use as button
3679  */
3680 OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
3681         if ( this.$button ) {
3682                 this.$button
3683                         .removeClass( 'oo-ui-buttonElement-button' )
3684                         .removeAttr( 'role accesskey' )
3685                         .off( {
3686                                 mousedown: this.onMouseDownHandler,
3687                                 keydown: this.onKeyDownHandler,
3688                                 click: this.onClickHandler,
3689                                 keypress: this.onKeyPressHandler
3690                         } );
3691         }
3693         this.$button = $button
3694                 .addClass( 'oo-ui-buttonElement-button' )
3695                 .attr( { role: 'button', accesskey: this.accessKey } )
3696                 .on( {
3697                         mousedown: this.onMouseDownHandler,
3698                         keydown: this.onKeyDownHandler,
3699                         click: this.onClickHandler,
3700                         keypress: this.onKeyPressHandler
3701                 } );
3705  * Handles mouse down events.
3707  * @protected
3708  * @param {jQuery.Event} e Mouse down event
3709  */
3710 OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
3711         if ( this.isDisabled() || e.which !== 1 ) {
3712                 return;
3713         }
3714         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3715         // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3716         // reliably remove the pressed class
3717         this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
3718         // Prevent change of focus unless specifically configured otherwise
3719         if ( this.constructor.static.cancelButtonMouseDownEvents ) {
3720                 return false;
3721         }
3725  * Handles mouse up events.
3727  * @protected
3728  * @param {jQuery.Event} e Mouse up event
3729  */
3730 OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
3731         if ( this.isDisabled() || e.which !== 1 ) {
3732                 return;
3733         }
3734         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3735         // Stop listening for mouseup, since we only needed this once
3736         this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
3740  * Handles mouse click events.
3742  * @protected
3743  * @param {jQuery.Event} e Mouse click event
3744  * @fires click
3745  */
3746 OO.ui.ButtonElement.prototype.onClick = function ( e ) {
3747         if ( !this.isDisabled() && e.which === 1 ) {
3748                 this.emit( 'click' );
3749         }
3750         return false;
3754  * Handles key down events.
3756  * @protected
3757  * @param {jQuery.Event} e Key down event
3758  */
3759 OO.ui.ButtonElement.prototype.onKeyDown = function ( e ) {
3760         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
3761                 return;
3762         }
3763         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3764         // Run the keyup handler no matter where the key is when the button is let go, so we can
3765         // reliably remove the pressed class
3766         this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
3770  * Handles key up events.
3772  * @protected
3773  * @param {jQuery.Event} e Key up event
3774  */
3775 OO.ui.ButtonElement.prototype.onKeyUp = function ( e ) {
3776         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
3777                 return;
3778         }
3779         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3780         // Stop listening for keyup, since we only needed this once
3781         this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
3785  * Handles key press events.
3787  * @protected
3788  * @param {jQuery.Event} e Key press event
3789  * @fires click
3790  */
3791 OO.ui.ButtonElement.prototype.onKeyPress = function ( e ) {
3792         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
3793                 this.emit( 'click' );
3794         }
3795         return false;
3799  * Check if button has a frame.
3801  * @return {boolean} Button is framed
3802  */
3803 OO.ui.ButtonElement.prototype.isFramed = function () {
3804         return this.framed;
3808  * Toggle frame.
3810  * @param {boolean} [framed] Make button framed, omit to toggle
3811  * @chainable
3812  */
3813 OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) {
3814         framed = framed === undefined ? !this.framed : !!framed;
3815         if ( framed !== this.framed ) {
3816                 this.framed = framed;
3817                 this.$element
3818                         .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
3819                         .toggleClass( 'oo-ui-buttonElement-framed', framed );
3820                 this.updateThemeClasses();
3821         }
3823         return this;
3827  * Set access key.
3829  * @param {string} accessKey Button's access key, use empty string to remove
3830  * @chainable
3831  */
3832 OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
3833         accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
3835         if ( this.accessKey !== accessKey ) {
3836                 if ( this.$button ) {
3837                         if ( accessKey !== null ) {
3838                                 this.$button.attr( 'accesskey', accessKey );
3839                         } else {
3840                                 this.$button.removeAttr( 'accesskey' );
3841                         }
3842                 }
3843                 this.accessKey = accessKey;
3844         }
3846         return this;
3850  * Set active state.
3852  * @param {boolean} [value] Make button active
3853  * @chainable
3854  */
3855 OO.ui.ButtonElement.prototype.setActive = function ( value ) {
3856         this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
3857         return this;
3861  * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
3862  * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
3863  * items from the group is done through the interface the class provides.
3864  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
3866  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
3868  * @abstract
3869  * @class
3871  * @constructor
3872  * @param {Object} [config] Configuration options
3873  * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
3874  */
3875 OO.ui.GroupElement = function OoUiGroupElement( config ) {
3876         // Configuration initialization
3877         config = config || {};
3879         // Properties
3880         this.$group = null;
3881         this.items = [];
3882         this.aggregateItemEvents = {};
3884         // Initialization
3885         this.setGroupElement( config.$group || $( '<div>' ) );
3888 /* Methods */
3891  * Set the group element.
3893  * If an element is already set, items will be moved to the new element.
3895  * @param {jQuery} $group Element to use as group
3896  */
3897 OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) {
3898         var i, len;
3900         this.$group = $group;
3901         for ( i = 0, len = this.items.length; i < len; i++ ) {
3902                 this.$group.append( this.items[ i ].$element );
3903         }
3907  * Check if there are no items.
3909  * @return {boolean} Group is empty
3910  */
3911 OO.ui.GroupElement.prototype.isEmpty = function () {
3912         return !this.items.length;
3916  * Get items.
3918  * @return {OO.ui.Element[]} Items
3919  */
3920 OO.ui.GroupElement.prototype.getItems = function () {
3921         return this.items.slice( 0 );
3925  * Get an item by its data.
3927  * Data is compared by a hash of its value. Only the first item with matching data will be returned.
3929  * @param {Object} data Item data to search for
3930  * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
3931  */
3932 OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
3933         var i, len, item,
3934                 hash = OO.getHash( data );
3936         for ( i = 0, len = this.items.length; i < len; i++ ) {
3937                 item = this.items[ i ];
3938                 if ( hash === OO.getHash( item.getData() ) ) {
3939                         return item;
3940                 }
3941         }
3943         return null;
3947  * Get items by their data.
3949  * Data is compared by a hash of its value. All items with matching data will be returned.
3951  * @param {Object} data Item data to search for
3952  * @return {OO.ui.Element[]} Items with equivalent data
3953  */
3954 OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
3955         var i, len, item,
3956                 hash = OO.getHash( data ),
3957                 items = [];
3959         for ( i = 0, len = this.items.length; i < len; i++ ) {
3960                 item = this.items[ i ];
3961                 if ( hash === OO.getHash( item.getData() ) ) {
3962                         items.push( item );
3963                 }
3964         }
3966         return items;
3970  * Add an aggregate item event.
3972  * Aggregated events are listened to on each item and then emitted by the group under a new name,
3973  * and with an additional leading parameter containing the item that emitted the original event.
3974  * Other arguments that were emitted from the original event are passed through.
3976  * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
3977  *   event, use null value to remove aggregation
3978  * @throws {Error} If aggregation already exists
3979  */
3980 OO.ui.GroupElement.prototype.aggregate = function ( events ) {
3981         var i, len, item, add, remove, itemEvent, groupEvent;
3983         for ( itemEvent in events ) {
3984                 groupEvent = events[ itemEvent ];
3986                 // Remove existing aggregated event
3987                 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
3988                         // Don't allow duplicate aggregations
3989                         if ( groupEvent ) {
3990                                 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
3991                         }
3992                         // Remove event aggregation from existing items
3993                         for ( i = 0, len = this.items.length; i < len; i++ ) {
3994                                 item = this.items[ i ];
3995                                 if ( item.connect && item.disconnect ) {
3996                                         remove = {};
3997                                         remove[ itemEvent ] = [ 'emit', groupEvent, item ];
3998                                         item.disconnect( this, remove );
3999                                 }
4000                         }
4001                         // Prevent future items from aggregating event
4002                         delete this.aggregateItemEvents[ itemEvent ];
4003                 }
4005                 // Add new aggregate event
4006                 if ( groupEvent ) {
4007                         // Make future items aggregate event
4008                         this.aggregateItemEvents[ itemEvent ] = groupEvent;
4009                         // Add event aggregation to existing items
4010                         for ( i = 0, len = this.items.length; i < len; i++ ) {
4011                                 item = this.items[ i ];
4012                                 if ( item.connect && item.disconnect ) {
4013                                         add = {};
4014                                         add[ itemEvent ] = [ 'emit', groupEvent, item ];
4015                                         item.connect( this, add );
4016                                 }
4017                         }
4018                 }
4019         }
4023  * Add items.
4025  * Adding an existing item will move it.
4027  * @param {OO.ui.Element[]} items Items
4028  * @param {number} [index] Index to insert items at
4029  * @chainable
4030  */
4031 OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
4032         var i, len, item, event, events, currentIndex,
4033                 itemElements = [];
4035         for ( i = 0, len = items.length; i < len; i++ ) {
4036                 item = items[ i ];
4038                 // Check if item exists then remove it first, effectively "moving" it
4039                 currentIndex = $.inArray( item, this.items );
4040                 if ( currentIndex >= 0 ) {
4041                         this.removeItems( [ item ] );
4042                         // Adjust index to compensate for removal
4043                         if ( currentIndex < index ) {
4044                                 index--;
4045                         }
4046                 }
4047                 // Add the item
4048                 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4049                         events = {};
4050                         for ( event in this.aggregateItemEvents ) {
4051                                 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4052                         }
4053                         item.connect( this, events );
4054                 }
4055                 item.setElementGroup( this );
4056                 itemElements.push( item.$element.get( 0 ) );
4057         }
4059         if ( index === undefined || index < 0 || index >= this.items.length ) {
4060                 this.$group.append( itemElements );
4061                 this.items.push.apply( this.items, items );
4062         } else if ( index === 0 ) {
4063                 this.$group.prepend( itemElements );
4064                 this.items.unshift.apply( this.items, items );
4065         } else {
4066                 this.items[ index ].$element.before( itemElements );
4067                 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4068         }
4070         return this;
4074  * Remove items.
4076  * Items will be detached, not removed, so they can be used later.
4078  * @param {OO.ui.Element[]} items Items to remove
4079  * @chainable
4080  */
4081 OO.ui.GroupElement.prototype.removeItems = function ( items ) {
4082         var i, len, item, index, remove, itemEvent;
4084         // Remove specific items
4085         for ( i = 0, len = items.length; i < len; i++ ) {
4086                 item = items[ i ];
4087                 index = $.inArray( item, this.items );
4088                 if ( index !== -1 ) {
4089                         if (
4090                                 item.connect && item.disconnect &&
4091                                 !$.isEmptyObject( this.aggregateItemEvents )
4092                         ) {
4093                                 remove = {};
4094                                 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4095                                         remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4096                                 }
4097                                 item.disconnect( this, remove );
4098                         }
4099                         item.setElementGroup( null );
4100                         this.items.splice( index, 1 );
4101                         item.$element.detach();
4102                 }
4103         }
4105         return this;
4109  * Clear all items.
4111  * Items will be detached, not removed, so they can be used later.
4113  * @chainable
4114  */
4115 OO.ui.GroupElement.prototype.clearItems = function () {
4116         var i, len, item, remove, itemEvent;
4118         // Remove all items
4119         for ( i = 0, len = this.items.length; i < len; i++ ) {
4120                 item = this.items[ i ];
4121                 if (
4122                         item.connect && item.disconnect &&
4123                         !$.isEmptyObject( this.aggregateItemEvents )
4124                 ) {
4125                         remove = {};
4126                         if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4127                                 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4128                         }
4129                         item.disconnect( this, remove );
4130                 }
4131                 item.setElementGroup( null );
4132                 item.$element.detach();
4133         }
4135         this.items = [];
4136         return this;
4140  * DraggableElement is a mixin class used to create elements that can be clicked
4141  * and dragged by a mouse to a new position within a group. This class must be used
4142  * in conjunction with OO.ui.DraggableGroupElement, which provides a container for
4143  * the draggable elements.
4145  * @abstract
4146  * @class
4148  * @constructor
4149  */
4150 OO.ui.DraggableElement = function OoUiDraggableElement() {
4151         // Properties
4152         this.index = null;
4154         // Initialize and events
4155         this.$element
4156                 .attr( 'draggable', true )
4157                 .addClass( 'oo-ui-draggableElement' )
4158                 .on( {
4159                         dragstart: this.onDragStart.bind( this ),
4160                         dragover: this.onDragOver.bind( this ),
4161                         dragend: this.onDragEnd.bind( this ),
4162                         drop: this.onDrop.bind( this )
4163                 } );
4166 OO.initClass( OO.ui.DraggableElement );
4168 /* Events */
4171  * @event dragstart
4172  * @param {OO.ui.DraggableElement} item Dragging item
4173  */
4176  * @event dragend
4177  */
4180  * @event drop
4181  */
4183 /* Static Properties */
4186  * @inheritdoc OO.ui.ButtonElement
4187  */
4188 OO.ui.DraggableElement.static.cancelButtonMouseDownEvents = false;
4190 /* Methods */
4193  * Respond to dragstart event.
4194  * @param {jQuery.Event} event jQuery event
4195  * @fires dragstart
4196  */
4197 OO.ui.DraggableElement.prototype.onDragStart = function ( e ) {
4198         var dataTransfer = e.originalEvent.dataTransfer;
4199         // Define drop effect
4200         dataTransfer.dropEffect = 'none';
4201         dataTransfer.effectAllowed = 'move';
4202         // We must set up a dataTransfer data property or Firefox seems to
4203         // ignore the fact the element is draggable.
4204         try {
4205                 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4206         } catch ( err ) {
4207                 // The above is only for firefox. No need to set a catch clause
4208                 // if it fails, move on.
4209         }
4210         // Add dragging class
4211         this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4212         // Emit event
4213         this.emit( 'dragstart', this );
4214         return true;
4218  * Respond to dragend event.
4219  * @fires dragend
4220  */
4221 OO.ui.DraggableElement.prototype.onDragEnd = function () {
4222         this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4223         this.emit( 'dragend' );
4227  * Handle drop event.
4228  * @param {jQuery.Event} event jQuery event
4229  * @fires drop
4230  */
4231 OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
4232         e.preventDefault();
4233         this.emit( 'drop', e );
4237  * In order for drag/drop to work, the dragover event must
4238  * return false and stop propogation.
4239  */
4240 OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
4241         e.preventDefault();
4245  * Set item index.
4246  * Store it in the DOM so we can access from the widget drag event
4247  * @param {number} Item index
4248  */
4249 OO.ui.DraggableElement.prototype.setIndex = function ( index ) {
4250         if ( this.index !== index ) {
4251                 this.index = index;
4252                 this.$element.data( 'index', index );
4253         }
4257  * Get item index
4258  * @return {number} Item index
4259  */
4260 OO.ui.DraggableElement.prototype.getIndex = function () {
4261         return this.index;
4265  * DraggableGroupElement is a mixin class used to create a group element to
4266  * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4267  * The class is used with OO.ui.DraggableElement.
4269  * @abstract
4270  * @class
4272  * @constructor
4273  * @param {Object} [config] Configuration options
4274  * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4275  * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
4276  */
4277 OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
4278         // Configuration initialization
4279         config = config || {};
4281         // Parent constructor
4282         OO.ui.GroupElement.call( this, config );
4284         // Properties
4285         this.orientation = config.orientation || 'vertical';
4286         this.dragItem = null;
4287         this.itemDragOver = null;
4288         this.itemKeys = {};
4289         this.sideInsertion = '';
4291         // Events
4292         this.aggregate( {
4293                 dragstart: 'itemDragStart',
4294                 dragend: 'itemDragEnd',
4295                 drop: 'itemDrop'
4296         } );
4297         this.connect( this, {
4298                 itemDragStart: 'onItemDragStart',
4299                 itemDrop: 'onItemDrop',
4300                 itemDragEnd: 'onItemDragEnd'
4301         } );
4302         this.$element.on( {
4303                 dragover: $.proxy( this.onDragOver, this ),
4304                 dragleave: $.proxy( this.onDragLeave, this )
4305         } );
4307         // Initialize
4308         if ( Array.isArray( config.items ) ) {
4309                 this.addItems( config.items );
4310         }
4311         this.$placeholder = $( '<div>' )
4312                 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4313         this.$element
4314                 .addClass( 'oo-ui-draggableGroupElement' )
4315                 .append( this.$status )
4316                 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4317                 .prepend( this.$placeholder );
4320 /* Setup */
4321 OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
4323 /* Events */
4326  * @event reorder
4327  * @param {OO.ui.DraggableElement} item Reordered item
4328  * @param {number} [newIndex] New index for the item
4329  */
4331 /* Methods */
4334  * Respond to item drag start event
4335  * @param {OO.ui.DraggableElement} item Dragged item
4336  */
4337 OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4338         var i, len;
4340         // Map the index of each object
4341         for ( i = 0, len = this.items.length; i < len; i++ ) {
4342                 this.items[ i ].setIndex( i );
4343         }
4345         if ( this.orientation === 'horizontal' ) {
4346                 // Set the height of the indicator
4347                 this.$placeholder.css( {
4348                         height: item.$element.outerHeight(),
4349                         width: 2
4350                 } );
4351         } else {
4352                 // Set the width of the indicator
4353                 this.$placeholder.css( {
4354                         height: 2,
4355                         width: item.$element.outerWidth()
4356                 } );
4357         }
4358         this.setDragItem( item );
4362  * Respond to item drag end event
4363  */
4364 OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
4365         this.unsetDragItem();
4366         return false;
4370  * Handle drop event and switch the order of the items accordingly
4371  * @param {OO.ui.DraggableElement} item Dropped item
4372  * @fires reorder
4373  */
4374 OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
4375         var toIndex = item.getIndex();
4376         // Check if the dropped item is from the current group
4377         // TODO: Figure out a way to configure a list of legally droppable
4378         // elements even if they are not yet in the list
4379         if ( this.getDragItem() ) {
4380                 // If the insertion point is 'after', the insertion index
4381                 // is shifted to the right (or to the left in RTL, hence 'after')
4382                 if ( this.sideInsertion === 'after' ) {
4383                         toIndex++;
4384                 }
4385                 // Emit change event
4386                 this.emit( 'reorder', this.getDragItem(), toIndex );
4387         }
4388         this.unsetDragItem();
4389         // Return false to prevent propogation
4390         return false;
4394  * Handle dragleave event.
4395  */
4396 OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
4397         // This means the item was dragged outside the widget
4398         this.$placeholder
4399                 .css( 'left', 0 )
4400                 .addClass( 'oo-ui-element-hidden' );
4404  * Respond to dragover event
4405  * @param {jQuery.Event} event Event details
4406  */
4407 OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
4408         var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
4409                 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
4410                 clientX = e.originalEvent.clientX,
4411                 clientY = e.originalEvent.clientY;
4413         // Get the OptionWidget item we are dragging over
4414         dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
4415         $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
4416         if ( $optionWidget[ 0 ] ) {
4417                 itemOffset = $optionWidget.offset();
4418                 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
4419                 itemPosition = $optionWidget.position();
4420                 itemIndex = $optionWidget.data( 'index' );
4421         }
4423         if (
4424                 itemOffset &&
4425                 this.isDragging() &&
4426                 itemIndex !== this.getDragItem().getIndex()
4427         ) {
4428                 if ( this.orientation === 'horizontal' ) {
4429                         // Calculate where the mouse is relative to the item width
4430                         itemSize = itemBoundingRect.width;
4431                         itemMidpoint = itemBoundingRect.left + itemSize / 2;
4432                         dragPosition = clientX;
4433                         // Which side of the item we hover over will dictate
4434                         // where the placeholder will appear, on the left or
4435                         // on the right
4436                         cssOutput = {
4437                                 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
4438                                 top: itemPosition.top
4439                         };
4440                 } else {
4441                         // Calculate where the mouse is relative to the item height
4442                         itemSize = itemBoundingRect.height;
4443                         itemMidpoint = itemBoundingRect.top + itemSize / 2;
4444                         dragPosition = clientY;
4445                         // Which side of the item we hover over will dictate
4446                         // where the placeholder will appear, on the top or
4447                         // on the bottom
4448                         cssOutput = {
4449                                 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
4450                                 left: itemPosition.left
4451                         };
4452                 }
4453                 // Store whether we are before or after an item to rearrange
4454                 // For horizontal layout, we need to account for RTL, as this is flipped
4455                 if (  this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
4456                         this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
4457                 } else {
4458                         this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
4459                 }
4460                 // Add drop indicator between objects
4461                 this.$placeholder
4462                         .css( cssOutput )
4463                         .removeClass( 'oo-ui-element-hidden' );
4464         } else {
4465                 // This means the item was dragged outside the widget
4466                 this.$placeholder
4467                         .css( 'left', 0 )
4468                         .addClass( 'oo-ui-element-hidden' );
4469         }
4470         // Prevent default
4471         e.preventDefault();
4475  * Set a dragged item
4476  * @param {OO.ui.DraggableElement} item Dragged item
4477  */
4478 OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
4479         this.dragItem = item;
4483  * Unset the current dragged item
4484  */
4485 OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
4486         this.dragItem = null;
4487         this.itemDragOver = null;
4488         this.$placeholder.addClass( 'oo-ui-element-hidden' );
4489         this.sideInsertion = '';
4493  * Get the current dragged item
4494  * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
4495  */
4496 OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
4497         return this.dragItem;
4501  * Check if there's an item being dragged.
4502  * @return {Boolean} Item is being dragged
4503  */
4504 OO.ui.DraggableGroupElement.prototype.isDragging = function () {
4505         return this.getDragItem() !== null;
4509  * IconElement is often mixed into other classes to generate an icon.
4510  * Icons are graphics, about the size of normal text. They are used to aid the user
4511  * in locating a control or to convey information in a space-efficient way. See the
4512  * [OOjs UI documentation on MediaWiki] [1] for a list of icons
4513  * included in the library.
4515  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4517  * @abstract
4518  * @class
4520  * @constructor
4521  * @param {Object} [config] Configuration options
4522  * @cfg {jQuery} [$icon] Icon node, assigned to #$icon, omit to use a generated `<span>`
4523  * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
4524  *  use the 'default' key to specify the icon to be used when there is no icon in the user's
4525  *  language
4526  * @cfg {string} [iconTitle] Icon title text or a function that returns text
4527  */
4528 OO.ui.IconElement = function OoUiIconElement( config ) {
4529         // Configuration initialization
4530         config = config || {};
4532         // Properties
4533         this.$icon = null;
4534         this.icon = null;
4535         this.iconTitle = null;
4537         // Initialization
4538         this.setIcon( config.icon || this.constructor.static.icon );
4539         this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
4540         this.setIconElement( config.$icon || $( '<span>' ) );
4543 /* Setup */
4545 OO.initClass( OO.ui.IconElement );
4547 /* Static Properties */
4550  * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
4551  * for i18n purposes and contains a `default` icon name and additional names keyed by
4552  * language code. The `default` name is used when no icon is keyed by the user's language.
4554  * Example of an i18n map:
4556  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4558  * Note: the static property will be overridden if the #icon configuration is used.
4560  * @static
4561  * @inheritable
4562  * @property {Object|string}
4563  */
4564 OO.ui.IconElement.static.icon = null;
4567  * The icon title, displayed when users move the mouse over the icon. The value can be text, a
4568  * function that returns title text, or `null` for no title.
4570  * The static property will be overridden if the #iconTitle configuration is used.
4572  * @static
4573  * @inheritable
4574  * @property {string|Function|null}
4575  */
4576 OO.ui.IconElement.static.iconTitle = null;
4578 /* Methods */
4581  * Set the icon element.
4583  * If an element is already set, it will be cleaned up before setting up the new element.
4585  * @param {jQuery} $icon Element to use as icon
4586  */
4587 OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
4588         if ( this.$icon ) {
4589                 this.$icon
4590                         .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
4591                         .removeAttr( 'title' );
4592         }
4594         this.$icon = $icon
4595                 .addClass( 'oo-ui-iconElement-icon' )
4596                 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
4597         if ( this.iconTitle !== null ) {
4598                 this.$icon.attr( 'title', this.iconTitle );
4599         }
4603  * Set icon name.
4605  * @param {Object|string|null} icon Symbolic icon name, or map of icon names keyed by language ID;
4606  *  use the 'default' key to specify the icon to be used when there is no icon in the user's
4607  *  language, use null to remove icon
4608  * @chainable
4609  */
4610 OO.ui.IconElement.prototype.setIcon = function ( icon ) {
4611         icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
4612         icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
4614         if ( this.icon !== icon ) {
4615                 if ( this.$icon ) {
4616                         if ( this.icon !== null ) {
4617                                 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
4618                         }
4619                         if ( icon !== null ) {
4620                                 this.$icon.addClass( 'oo-ui-icon-' + icon );
4621                         }
4622                 }
4623                 this.icon = icon;
4624         }
4626         this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
4627         this.updateThemeClasses();
4629         return this;
4633  * Set icon title.
4635  * @param {string|Function|null} icon Icon title text, a function that returns text or null
4636  *  for no icon title
4637  * @chainable
4638  */
4639 OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
4640         iconTitle = typeof iconTitle === 'function' ||
4641                 ( typeof iconTitle === 'string' && iconTitle.length ) ?
4642                         OO.ui.resolveMsg( iconTitle ) : null;
4644         if ( this.iconTitle !== iconTitle ) {
4645                 this.iconTitle = iconTitle;
4646                 if ( this.$icon ) {
4647                         if ( this.iconTitle !== null ) {
4648                                 this.$icon.attr( 'title', iconTitle );
4649                         } else {
4650                                 this.$icon.removeAttr( 'title' );
4651                         }
4652                 }
4653         }
4655         return this;
4659  * Get icon name.
4661  * @return {string} Icon name
4662  */
4663 OO.ui.IconElement.prototype.getIcon = function () {
4664         return this.icon;
4668  * Get icon title.
4670  * @return {string} Icon title text
4671  */
4672 OO.ui.IconElement.prototype.getIconTitle = function () {
4673         return this.iconTitle;
4677  * IndicatorElement is often mixed into other classes to generate an indicator.
4678  * Indicators are small graphics that are generally used in two ways:
4680  * - To draw attention to the status of an item. For example, an indicator might be
4681  *   used to show that an item in a list has errors that need to be resolved.
4682  * - To clarify the function of a control that acts in an exceptional way (a button
4683  *   that opens a menu instead of performing an action directly, for example).
4685  * For a list of indicators included in the library, please see the
4686  * [OOjs UI documentation on MediaWiki] [1].
4688  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4690  * @abstract
4691  * @class
4693  * @constructor
4694  * @param {Object} [config] Configuration options
4695  * @cfg {jQuery} [$indicator] Indicator node, assigned to #$indicator, omit to use a generated
4696  *   `<span>`
4697  * @cfg {string} [indicator] Symbolic indicator name
4698  * @cfg {string} [indicatorTitle] Indicator title text or a function that returns text
4699  */
4700 OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
4701         // Configuration initialization
4702         config = config || {};
4704         // Properties
4705         this.$indicator = null;
4706         this.indicator = null;
4707         this.indicatorTitle = null;
4709         // Initialization
4710         this.setIndicator( config.indicator || this.constructor.static.indicator );
4711         this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
4712         this.setIndicatorElement( config.$indicator || $( '<span>' ) );
4715 /* Setup */
4717 OO.initClass( OO.ui.IndicatorElement );
4719 /* Static Properties */
4722  * indicator.
4724  * @static
4725  * @inheritable
4726  * @property {string|null} Symbolic indicator name
4727  */
4728 OO.ui.IndicatorElement.static.indicator = null;
4731  * Indicator title.
4733  * @static
4734  * @inheritable
4735  * @property {string|Function|null} Indicator title text, a function that returns text or null for no
4736  *  indicator title
4737  */
4738 OO.ui.IndicatorElement.static.indicatorTitle = null;
4740 /* Methods */
4743  * Set the indicator element.
4745  * If an element is already set, it will be cleaned up before setting up the new element.
4747  * @param {jQuery} $indicator Element to use as indicator
4748  */
4749 OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
4750         if ( this.$indicator ) {
4751                 this.$indicator
4752                         .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
4753                         .removeAttr( 'title' );
4754         }
4756         this.$indicator = $indicator
4757                 .addClass( 'oo-ui-indicatorElement-indicator' )
4758                 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
4759         if ( this.indicatorTitle !== null ) {
4760                 this.$indicator.attr( 'title', this.indicatorTitle );
4761         }
4765  * Set indicator name.
4767  * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
4768  * @chainable
4769  */
4770 OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
4771         indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
4773         if ( this.indicator !== indicator ) {
4774                 if ( this.$indicator ) {
4775                         if ( this.indicator !== null ) {
4776                                 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
4777                         }
4778                         if ( indicator !== null ) {
4779                                 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
4780                         }
4781                 }
4782                 this.indicator = indicator;
4783         }
4785         this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
4786         this.updateThemeClasses();
4788         return this;
4792  * Set indicator title.
4794  * @param {string|Function|null} indicator Indicator title text, a function that returns text or
4795  *   null for no indicator title
4796  * @chainable
4797  */
4798 OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
4799         indicatorTitle = typeof indicatorTitle === 'function' ||
4800                 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
4801                         OO.ui.resolveMsg( indicatorTitle ) : null;
4803         if ( this.indicatorTitle !== indicatorTitle ) {
4804                 this.indicatorTitle = indicatorTitle;
4805                 if ( this.$indicator ) {
4806                         if ( this.indicatorTitle !== null ) {
4807                                 this.$indicator.attr( 'title', indicatorTitle );
4808                         } else {
4809                                 this.$indicator.removeAttr( 'title' );
4810                         }
4811                 }
4812         }
4814         return this;
4818  * Get indicator name.
4820  * @return {string} Symbolic name of indicator
4821  */
4822 OO.ui.IndicatorElement.prototype.getIndicator = function () {
4823         return this.indicator;
4827  * Get indicator title.
4829  * @return {string} Indicator title text
4830  */
4831 OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
4832         return this.indicatorTitle;
4836  * Element containing a label.
4838  * @abstract
4839  * @class
4841  * @constructor
4842  * @param {Object} [config] Configuration options
4843  * @cfg {jQuery} [$label] Label node, assigned to #$label, omit to use a generated `<span>`
4844  * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
4845  * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
4846  */
4847 OO.ui.LabelElement = function OoUiLabelElement( config ) {
4848         // Configuration initialization
4849         config = config || {};
4851         // Properties
4852         this.$label = null;
4853         this.label = null;
4854         this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
4856         // Initialization
4857         this.setLabel( config.label || this.constructor.static.label );
4858         this.setLabelElement( config.$label || $( '<span>' ) );
4861 /* Setup */
4863 OO.initClass( OO.ui.LabelElement );
4865 /* Events */
4868  * @event labelChange
4869  * @param {string} value
4870  */
4872 /* Static Properties */
4875  * Label.
4877  * @static
4878  * @inheritable
4879  * @property {string|Function|null} Label text; a function that returns nodes or text; or null for
4880  *  no label
4881  */
4882 OO.ui.LabelElement.static.label = null;
4884 /* Methods */
4887  * Set the label element.
4889  * If an element is already set, it will be cleaned up before setting up the new element.
4891  * @param {jQuery} $label Element to use as label
4892  */
4893 OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) {
4894         if ( this.$label ) {
4895                 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
4896         }
4898         this.$label = $label.addClass( 'oo-ui-labelElement-label' );
4899         this.setLabelContent( this.label );
4903  * Set the label.
4905  * An empty string will result in the label being hidden. A string containing only whitespace will
4906  * be converted to a single `&nbsp;`.
4908  * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4909  *  text; or null for no label
4910  * @chainable
4911  */
4912 OO.ui.LabelElement.prototype.setLabel = function ( label ) {
4913         label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
4914         label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null;
4916         this.$element.toggleClass( 'oo-ui-labelElement', !!label );
4918         if ( this.label !== label ) {
4919                 if ( this.$label ) {
4920                         this.setLabelContent( label );
4921                 }
4922                 this.label = label;
4923                 this.emit( 'labelChange' );
4924         }
4926         return this;
4930  * Get the label.
4932  * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
4933  *  text; or null for no label
4934  */
4935 OO.ui.LabelElement.prototype.getLabel = function () {
4936         return this.label;
4940  * Fit the label.
4942  * @chainable
4943  */
4944 OO.ui.LabelElement.prototype.fitLabel = function () {
4945         if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
4946                 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
4947         }
4949         return this;
4953  * Set the content of the label.
4955  * Do not call this method until after the label element has been set by #setLabelElement.
4957  * @private
4958  * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4959  *  text; or null for no label
4960  */
4961 OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
4962         if ( typeof label === 'string' ) {
4963                 if ( label.match( /^\s*$/ ) ) {
4964                         // Convert whitespace only string to a single non-breaking space
4965                         this.$label.html( '&nbsp;' );
4966                 } else {
4967                         this.$label.text( label );
4968                 }
4969         } else if ( label instanceof jQuery ) {
4970                 this.$label.empty().append( label );
4971         } else {
4972                 this.$label.empty();
4973         }
4977  * Mixin that adds a menu showing suggested values for a OO.ui.TextInputWidget.
4979  * Subclasses that set the value of #lookupInput from #onLookupMenuItemChoose should
4980  * be aware that this will cause new suggestions to be looked up for the new value. If this is
4981  * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
4983  * @class
4984  * @abstract
4986  * @constructor
4987  * @param {Object} [config] Configuration options
4988  * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
4989  * @cfg {jQuery} [$container=this.$element] Element to render menu under
4990  */
4991 OO.ui.LookupElement = function OoUiLookupElement( config ) {
4992         // Configuration initialization
4993         config = config || {};
4995         // Properties
4996         this.$overlay = config.$overlay || this.$element;
4997         this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
4998                 widget: this,
4999                 input: this,
5000                 $container: config.$container
5001         } );
5002         this.lookupCache = {};
5003         this.lookupQuery = null;
5004         this.lookupRequest = null;
5005         this.lookupsDisabled = false;
5006         this.lookupInputFocused = false;
5008         // Events
5009         this.$input.on( {
5010                 focus: this.onLookupInputFocus.bind( this ),
5011                 blur: this.onLookupInputBlur.bind( this ),
5012                 mousedown: this.onLookupInputMouseDown.bind( this )
5013         } );
5014         this.connect( this, { change: 'onLookupInputChange' } );
5015         this.lookupMenu.connect( this, {
5016                 toggle: 'onLookupMenuToggle',
5017                 choose: 'onLookupMenuItemChoose'
5018         } );
5020         // Initialization
5021         this.$element.addClass( 'oo-ui-lookupElement' );
5022         this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5023         this.$overlay.append( this.lookupMenu.$element );
5026 /* Methods */
5029  * Handle input focus event.
5031  * @param {jQuery.Event} e Input focus event
5032  */
5033 OO.ui.LookupElement.prototype.onLookupInputFocus = function () {
5034         this.lookupInputFocused = true;
5035         this.populateLookupMenu();
5039  * Handle input blur event.
5041  * @param {jQuery.Event} e Input blur event
5042  */
5043 OO.ui.LookupElement.prototype.onLookupInputBlur = function () {
5044         this.closeLookupMenu();
5045         this.lookupInputFocused = false;
5049  * Handle input mouse down event.
5051  * @param {jQuery.Event} e Input mouse down event
5052  */
5053 OO.ui.LookupElement.prototype.onLookupInputMouseDown = function () {
5054         // Only open the menu if the input was already focused.
5055         // This way we allow the user to open the menu again after closing it with Esc
5056         // by clicking in the input. Opening (and populating) the menu when initially
5057         // clicking into the input is handled by the focus handler.
5058         if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5059                 this.populateLookupMenu();
5060         }
5064  * Handle input change event.
5066  * @param {string} value New input value
5067  */
5068 OO.ui.LookupElement.prototype.onLookupInputChange = function () {
5069         if ( this.lookupInputFocused ) {
5070                 this.populateLookupMenu();
5071         }
5075  * Handle the lookup menu being shown/hidden.
5077  * @param {boolean} visible Whether the lookup menu is now visible.
5078  */
5079 OO.ui.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5080         if ( !visible ) {
5081                 // When the menu is hidden, abort any active request and clear the menu.
5082                 // This has to be done here in addition to closeLookupMenu(), because
5083                 // MenuSelectWidget will close itself when the user presses Esc.
5084                 this.abortLookupRequest();
5085                 this.lookupMenu.clearItems();
5086         }
5090  * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5092  * @param {OO.ui.MenuOptionWidget|null} item Selected item
5093  */
5094 OO.ui.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5095         if ( item ) {
5096                 this.setValue( item.getData() );
5097         }
5101  * Get lookup menu.
5103  * @return {OO.ui.TextInputMenuSelectWidget}
5104  */
5105 OO.ui.LookupElement.prototype.getLookupMenu = function () {
5106         return this.lookupMenu;
5110  * Disable or re-enable lookups.
5112  * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5114  * @param {boolean} disabled Disable lookups
5115  */
5116 OO.ui.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
5117         this.lookupsDisabled = !!disabled;
5121  * Open the menu. If there are no entries in the menu, this does nothing.
5123  * @chainable
5124  */
5125 OO.ui.LookupElement.prototype.openLookupMenu = function () {
5126         if ( !this.lookupMenu.isEmpty() ) {
5127                 this.lookupMenu.toggle( true );
5128         }
5129         return this;
5133  * Close the menu, empty it, and abort any pending request.
5135  * @chainable
5136  */
5137 OO.ui.LookupElement.prototype.closeLookupMenu = function () {
5138         this.lookupMenu.toggle( false );
5139         this.abortLookupRequest();
5140         this.lookupMenu.clearItems();
5141         return this;
5145  * Request menu items based on the input's current value, and when they arrive,
5146  * populate the menu with these items and show the menu.
5148  * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5150  * @chainable
5151  */
5152 OO.ui.LookupElement.prototype.populateLookupMenu = function () {
5153         var widget = this,
5154                 value = this.getValue();
5156         if ( this.lookupsDisabled ) {
5157                 return;
5158         }
5160         // If the input is empty, clear the menu
5161         if ( value === '' ) {
5162                 this.closeLookupMenu();
5163         // Skip population if there is already a request pending for the current value
5164         } else if ( value !== this.lookupQuery ) {
5165                 this.getLookupMenuItems()
5166                         .done( function ( items ) {
5167                                 widget.lookupMenu.clearItems();
5168                                 if ( items.length ) {
5169                                         widget.lookupMenu
5170                                                 .addItems( items )
5171                                                 .toggle( true );
5172                                         widget.initializeLookupMenuSelection();
5173                                 } else {
5174                                         widget.lookupMenu.toggle( false );
5175                                 }
5176                         } )
5177                         .fail( function () {
5178                                 widget.lookupMenu.clearItems();
5179                         } );
5180         }
5182         return this;
5186  * Select and highlight the first selectable item in the menu.
5188  * @chainable
5189  */
5190 OO.ui.LookupElement.prototype.initializeLookupMenuSelection = function () {
5191         if ( !this.lookupMenu.getSelectedItem() ) {
5192                 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
5193         }
5194         this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
5198  * Get lookup menu items for the current query.
5200  * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5201  *   the done event. If the request was aborted to make way for a subsequent request, this promise
5202  *   will not be rejected: it will remain pending forever.
5203  */
5204 OO.ui.LookupElement.prototype.getLookupMenuItems = function () {
5205         var widget = this,
5206                 value = this.getValue(),
5207                 deferred = $.Deferred(),
5208                 ourRequest;
5210         this.abortLookupRequest();
5211         if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5212                 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5213         } else {
5214                 this.pushPending();
5215                 this.lookupQuery = value;
5216                 ourRequest = this.lookupRequest = this.getLookupRequest();
5217                 ourRequest
5218                         .always( function () {
5219                                 // We need to pop pending even if this is an old request, otherwise
5220                                 // the widget will remain pending forever.
5221                                 // TODO: this assumes that an aborted request will fail or succeed soon after
5222                                 // being aborted, or at least eventually. It would be nice if we could popPending()
5223                                 // at abort time, but only if we knew that we hadn't already called popPending()
5224                                 // for that request.
5225                                 widget.popPending();
5226                         } )
5227                         .done( function ( data ) {
5228                                 // If this is an old request (and aborting it somehow caused it to still succeed),
5229                                 // ignore its success completely
5230                                 if ( ourRequest === widget.lookupRequest ) {
5231                                         widget.lookupQuery = null;
5232                                         widget.lookupRequest = null;
5233                                         widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( data );
5234                                         deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5235                                 }
5236                         } )
5237                         .fail( function () {
5238                                 // If this is an old request (or a request failing because it's being aborted),
5239                                 // ignore its failure completely
5240                                 if ( ourRequest === widget.lookupRequest ) {
5241                                         widget.lookupQuery = null;
5242                                         widget.lookupRequest = null;
5243                                         deferred.reject();
5244                                 }
5245                         } );
5246         }
5247         return deferred.promise();
5251  * Abort the currently pending lookup request, if any.
5252  */
5253 OO.ui.LookupElement.prototype.abortLookupRequest = function () {
5254         var oldRequest = this.lookupRequest;
5255         if ( oldRequest ) {
5256                 // First unset this.lookupRequest to the fail handler will notice
5257                 // that the request is no longer current
5258                 this.lookupRequest = null;
5259                 this.lookupQuery = null;
5260                 oldRequest.abort();
5261         }
5265  * Get a new request object of the current lookup query value.
5267  * @abstract
5268  * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5269  */
5270 OO.ui.LookupElement.prototype.getLookupRequest = function () {
5271         // Stub, implemented in subclass
5272         return null;
5276  * Pre-process data returned by the request from #getLookupRequest.
5278  * The return value of this function will be cached, and any further queries for the given value
5279  * will use the cache rather than doing API requests.
5281  * @abstract
5282  * @param {Mixed} data Response from server
5283  * @return {Mixed} Cached result data
5284  */
5285 OO.ui.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5286         // Stub, implemented in subclass
5287         return [];
5291  * Get a list of menu option widgets from the (possibly cached) data returned by
5292  * #getLookupCacheDataFromResponse.
5294  * @abstract
5295  * @param {Mixed} data Cached result data, usually an array
5296  * @return {OO.ui.MenuOptionWidget[]} Menu items
5297  */
5298 OO.ui.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
5299         // Stub, implemented in subclass
5300         return [];
5304  * Element containing an OO.ui.PopupWidget object.
5306  * @abstract
5307  * @class
5309  * @constructor
5310  * @param {Object} [config] Configuration options
5311  * @cfg {Object} [popup] Configuration to pass to popup
5312  * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5313  */
5314 OO.ui.PopupElement = function OoUiPopupElement( config ) {
5315         // Configuration initialization
5316         config = config || {};
5318         // Properties
5319         this.popup = new OO.ui.PopupWidget( $.extend(
5320                 { autoClose: true },
5321                 config.popup,
5322                 { $autoCloseIgnore: this.$element }
5323         ) );
5326 /* Methods */
5329  * Get popup.
5331  * @return {OO.ui.PopupWidget} Popup widget
5332  */
5333 OO.ui.PopupElement.prototype.getPopup = function () {
5334         return this.popup;
5338  * The FlaggedElement class is an attribute mixin, meaning that it is used to add
5339  * additional functionality to an element created by another class. The class provides
5340  * a ‘flags’ property assigned the name (or an array of names) of styling flags,
5341  * which are used to customize the look and feel of a widget to better describe its
5342  * importance and functionality.
5344  * The library currently contains the following styling flags for general use:
5346  * - **progressive**:  Progressive styling is applied to convey that the widget will move the user forward in a process.
5347  * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
5348  * - **constructive**: Constructive styling is applied to convey that the widget will create something.
5350  * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
5351  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
5353  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5355  * @abstract
5356  * @class
5358  * @constructor
5359  * @param {Object} [config] Configuration options
5360  * @cfg {string|string[]} [flags] Flags describing importance and functionality, e.g. 'primary',
5361  *   'safe', 'progressive', 'destructive' or 'constructive'
5362  * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element
5363  */
5364 OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
5365         // Configuration initialization
5366         config = config || {};
5368         // Properties
5369         this.flags = {};
5370         this.$flagged = null;
5372         // Initialization
5373         this.setFlags( config.flags );
5374         this.setFlaggedElement( config.$flagged || this.$element );
5377 /* Events */
5380  * @event flag
5381  * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
5382  *   added/removed properties
5383  */
5385 /* Methods */
5388  * Set the flagged element.
5390  * If an element is already set, it will be cleaned up before setting up the new element.
5392  * @param {jQuery} $flagged Element to add flags to
5393  */
5394 OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
5395         var classNames = Object.keys( this.flags ).map( function ( flag ) {
5396                 return 'oo-ui-flaggedElement-' + flag;
5397         } ).join( ' ' );
5399         if ( this.$flagged ) {
5400                 this.$flagged.removeClass( classNames );
5401         }
5403         this.$flagged = $flagged.addClass( classNames );
5407  * Check if a flag is set.
5409  * @param {string} flag Name of flag
5410  * @return {boolean} Has flag
5411  */
5412 OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) {
5413         return flag in this.flags;
5417  * Get the names of all flags set.
5419  * @return {string[]} Flag names
5420  */
5421 OO.ui.FlaggedElement.prototype.getFlags = function () {
5422         return Object.keys( this.flags );
5426  * Clear all flags.
5428  * @chainable
5429  * @fires flag
5430  */
5431 OO.ui.FlaggedElement.prototype.clearFlags = function () {
5432         var flag, className,
5433                 changes = {},
5434                 remove = [],
5435                 classPrefix = 'oo-ui-flaggedElement-';
5437         for ( flag in this.flags ) {
5438                 className = classPrefix + flag;
5439                 changes[ flag ] = false;
5440                 delete this.flags[ flag ];
5441                 remove.push( className );
5442         }
5444         if ( this.$flagged ) {
5445                 this.$flagged.removeClass( remove.join( ' ' ) );
5446         }
5448         this.updateThemeClasses();
5449         this.emit( 'flag', changes );
5451         return this;
5455  * Add one or more flags.
5457  * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
5458  *  keyed by flag name containing boolean set/remove instructions.
5459  * @chainable
5460  * @fires flag
5461  */
5462 OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
5463         var i, len, flag, className,
5464                 changes = {},
5465                 add = [],
5466                 remove = [],
5467                 classPrefix = 'oo-ui-flaggedElement-';
5469         if ( typeof flags === 'string' ) {
5470                 className = classPrefix + flags;
5471                 // Set
5472                 if ( !this.flags[ flags ] ) {
5473                         this.flags[ flags ] = true;
5474                         add.push( className );
5475                 }
5476         } else if ( Array.isArray( flags ) ) {
5477                 for ( i = 0, len = flags.length; i < len; i++ ) {
5478                         flag = flags[ i ];
5479                         className = classPrefix + flag;
5480                         // Set
5481                         if ( !this.flags[ flag ] ) {
5482                                 changes[ flag ] = true;
5483                                 this.flags[ flag ] = true;
5484                                 add.push( className );
5485                         }
5486                 }
5487         } else if ( OO.isPlainObject( flags ) ) {
5488                 for ( flag in flags ) {
5489                         className = classPrefix + flag;
5490                         if ( flags[ flag ] ) {
5491                                 // Set
5492                                 if ( !this.flags[ flag ] ) {
5493                                         changes[ flag ] = true;
5494                                         this.flags[ flag ] = true;
5495                                         add.push( className );
5496                                 }
5497                         } else {
5498                                 // Remove
5499                                 if ( this.flags[ flag ] ) {
5500                                         changes[ flag ] = false;
5501                                         delete this.flags[ flag ];
5502                                         remove.push( className );
5503                                 }
5504                         }
5505                 }
5506         }
5508         if ( this.$flagged ) {
5509                 this.$flagged
5510                         .addClass( add.join( ' ' ) )
5511                         .removeClass( remove.join( ' ' ) );
5512         }
5514         this.updateThemeClasses();
5515         this.emit( 'flag', changes );
5517         return this;
5521  * Element with a title.
5523  * Titles are rendered by the browser and are made visible when hovering the element. Titles are
5524  * not visible on touch devices.
5526  * @abstract
5527  * @class
5529  * @constructor
5530  * @param {Object} [config] Configuration options
5531  * @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element
5532  * @cfg {string|Function} [title] Title text or a function that returns text. If not provided, the
5533  *    static property 'title' is used.
5534  */
5535 OO.ui.TitledElement = function OoUiTitledElement( config ) {
5536         // Configuration initialization
5537         config = config || {};
5539         // Properties
5540         this.$titled = null;
5541         this.title = null;
5543         // Initialization
5544         this.setTitle( config.title || this.constructor.static.title );
5545         this.setTitledElement( config.$titled || this.$element );
5548 /* Setup */
5550 OO.initClass( OO.ui.TitledElement );
5552 /* Static Properties */
5555  * Title.
5557  * @static
5558  * @inheritable
5559  * @property {string|Function} Title text or a function that returns text
5560  */
5561 OO.ui.TitledElement.static.title = null;
5563 /* Methods */
5566  * Set the titled element.
5568  * If an element is already set, it will be cleaned up before setting up the new element.
5570  * @param {jQuery} $titled Element to set title on
5571  */
5572 OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
5573         if ( this.$titled ) {
5574                 this.$titled.removeAttr( 'title' );
5575         }
5577         this.$titled = $titled;
5578         if ( this.title ) {
5579                 this.$titled.attr( 'title', this.title );
5580         }
5584  * Set title.
5586  * @param {string|Function|null} title Title text, a function that returns text or null for no title
5587  * @chainable
5588  */
5589 OO.ui.TitledElement.prototype.setTitle = function ( title ) {
5590         title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
5592         if ( this.title !== title ) {
5593                 if ( this.$titled ) {
5594                         if ( title !== null ) {
5595                                 this.$titled.attr( 'title', title );
5596                         } else {
5597                                 this.$titled.removeAttr( 'title' );
5598                         }
5599                 }
5600                 this.title = title;
5601         }
5603         return this;
5607  * Get title.
5609  * @return {string} Title string
5610  */
5611 OO.ui.TitledElement.prototype.getTitle = function () {
5612         return this.title;
5616  * Element that can be automatically clipped to visible boundaries.
5618  * Whenever the element's natural height changes, you have to call
5619  * #clip to make sure it's still clipping correctly.
5621  * @abstract
5622  * @class
5624  * @constructor
5625  * @param {Object} [config] Configuration options
5626  * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
5627  */
5628 OO.ui.ClippableElement = function OoUiClippableElement( config ) {
5629         // Configuration initialization
5630         config = config || {};
5632         // Properties
5633         this.$clippable = null;
5634         this.clipping = false;
5635         this.clippedHorizontally = false;
5636         this.clippedVertically = false;
5637         this.$clippableContainer = null;
5638         this.$clippableScroller = null;
5639         this.$clippableWindow = null;
5640         this.idealWidth = null;
5641         this.idealHeight = null;
5642         this.onClippableContainerScrollHandler = this.clip.bind( this );
5643         this.onClippableWindowResizeHandler = this.clip.bind( this );
5645         // Initialization
5646         this.setClippableElement( config.$clippable || this.$element );
5649 /* Methods */
5652  * Set clippable element.
5654  * If an element is already set, it will be cleaned up before setting up the new element.
5656  * @param {jQuery} $clippable Element to make clippable
5657  */
5658 OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
5659         if ( this.$clippable ) {
5660                 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
5661                 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
5662                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5663         }
5665         this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
5666         this.clip();
5670  * Toggle clipping.
5672  * Do not turn clipping on until after the element is attached to the DOM and visible.
5674  * @param {boolean} [clipping] Enable clipping, omit to toggle
5675  * @chainable
5676  */
5677 OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
5678         clipping = clipping === undefined ? !this.clipping : !!clipping;
5680         if ( this.clipping !== clipping ) {
5681                 this.clipping = clipping;
5682                 if ( clipping ) {
5683                         this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
5684                         // If the clippable container is the root, we have to listen to scroll events and check
5685                         // jQuery.scrollTop on the window because of browser inconsistencies
5686                         this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
5687                                 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
5688                                 this.$clippableContainer;
5689                         this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
5690                         this.$clippableWindow = $( this.getElementWindow() )
5691                                 .on( 'resize', this.onClippableWindowResizeHandler );
5692                         // Initial clip after visible
5693                         this.clip();
5694                 } else {
5695                         this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
5696                         OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5698                         this.$clippableContainer = null;
5699                         this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
5700                         this.$clippableScroller = null;
5701                         this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
5702                         this.$clippableWindow = null;
5703                 }
5704         }
5706         return this;
5710  * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5712  * @return {boolean} Element will be clipped to the visible area
5713  */
5714 OO.ui.ClippableElement.prototype.isClipping = function () {
5715         return this.clipping;
5719  * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5721  * @return {boolean} Part of the element is being clipped
5722  */
5723 OO.ui.ClippableElement.prototype.isClipped = function () {
5724         return this.clippedHorizontally || this.clippedVertically;
5728  * Check if the right of the element is being clipped by the nearest scrollable container.
5730  * @return {boolean} Part of the element is being clipped
5731  */
5732 OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
5733         return this.clippedHorizontally;
5737  * Check if the bottom of the element is being clipped by the nearest scrollable container.
5739  * @return {boolean} Part of the element is being clipped
5740  */
5741 OO.ui.ClippableElement.prototype.isClippedVertically = function () {
5742         return this.clippedVertically;
5746  * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
5748  * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
5749  * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
5750  */
5751 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
5752         this.idealWidth = width;
5753         this.idealHeight = height;
5755         if ( !this.clipping ) {
5756                 // Update dimensions
5757                 this.$clippable.css( { width: width, height: height } );
5758         }
5759         // While clipping, idealWidth and idealHeight are not considered
5763  * Clip element to visible boundaries and allow scrolling when needed. Call this method when
5764  * the element's natural height changes.
5766  * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5767  * overlapped by, the visible area of the nearest scrollable container.
5769  * @chainable
5770  */
5771 OO.ui.ClippableElement.prototype.clip = function () {
5772         if ( !this.clipping ) {
5773                 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
5774                 return this;
5775         }
5777         var buffer = 7, // Chosen by fair dice roll
5778                 cOffset = this.$clippable.offset(),
5779                 $container = this.$clippableContainer.is( 'html, body' ) ?
5780                         this.$clippableWindow : this.$clippableContainer,
5781                 ccOffset = $container.offset() || { top: 0, left: 0 },
5782                 ccHeight = $container.innerHeight() - buffer,
5783                 ccWidth = $container.innerWidth() - buffer,
5784                 cHeight = this.$clippable.outerHeight() + buffer,
5785                 cWidth = this.$clippable.outerWidth() + buffer,
5786                 scrollTop = this.$clippableScroller.scrollTop(),
5787                 scrollLeft = this.$clippableScroller.scrollLeft(),
5788                 desiredWidth = cOffset.left < 0 ?
5789                         cWidth + cOffset.left :
5790                         ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
5791                 desiredHeight = cOffset.top < 0 ?
5792                         cHeight + cOffset.top :
5793                         ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
5794                 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
5795                 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
5796                 clipWidth = desiredWidth < naturalWidth,
5797                 clipHeight = desiredHeight < naturalHeight;
5799         if ( clipWidth ) {
5800                 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
5801         } else {
5802                 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
5803         }
5804         if ( clipHeight ) {
5805                 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
5806         } else {
5807                 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
5808         }
5810         // If we stopped clipping in at least one of the dimensions
5811         if ( !clipWidth || !clipHeight ) {
5812                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5813         }
5815         this.clippedHorizontally = clipWidth;
5816         this.clippedVertically = clipHeight;
5818         return this;
5822  * Generic toolbar tool.
5824  * @abstract
5825  * @class
5826  * @extends OO.ui.Widget
5827  * @mixins OO.ui.IconElement
5828  * @mixins OO.ui.FlaggedElement
5830  * @constructor
5831  * @param {OO.ui.ToolGroup} toolGroup
5832  * @param {Object} [config] Configuration options
5833  * @cfg {string|Function} [title] Title text or a function that returns text
5834  */
5835 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
5836         // Configuration initialization
5837         config = config || {};
5839         // Parent constructor
5840         OO.ui.Tool.super.call( this, config );
5842         // Mixin constructors
5843         OO.ui.IconElement.call( this, config );
5844         OO.ui.FlaggedElement.call( this, config );
5846         // Properties
5847         this.toolGroup = toolGroup;
5848         this.toolbar = this.toolGroup.getToolbar();
5849         this.active = false;
5850         this.$title = $( '<span>' );
5851         this.$accel = $( '<span>' );
5852         this.$link = $( '<a>' );
5853         this.title = null;
5855         // Events
5856         this.toolbar.connect( this, { updateState: 'onUpdateState' } );
5858         // Initialization
5859         this.$title.addClass( 'oo-ui-tool-title' );
5860         this.$accel
5861                 .addClass( 'oo-ui-tool-accel' )
5862                 .prop( {
5863                         // This may need to be changed if the key names are ever localized,
5864                         // but for now they are essentially written in English
5865                         dir: 'ltr',
5866                         lang: 'en'
5867                 } );
5868         this.$link
5869                 .addClass( 'oo-ui-tool-link' )
5870                 .append( this.$icon, this.$title, this.$accel )
5871                 .prop( 'tabIndex', 0 )
5872                 .attr( 'role', 'button' );
5873         this.$element
5874                 .data( 'oo-ui-tool', this )
5875                 .addClass(
5876                         'oo-ui-tool ' + 'oo-ui-tool-name-' +
5877                         this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
5878                 )
5879                 .append( this.$link );
5880         this.setTitle( config.title || this.constructor.static.title );
5883 /* Setup */
5885 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
5886 OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
5887 OO.mixinClass( OO.ui.Tool, OO.ui.FlaggedElement );
5889 /* Events */
5892  * @event select
5893  */
5895 /* Static Properties */
5898  * @static
5899  * @inheritdoc
5900  */
5901 OO.ui.Tool.static.tagName = 'span';
5904  * Symbolic name of tool.
5906  * @abstract
5907  * @static
5908  * @inheritable
5909  * @property {string}
5910  */
5911 OO.ui.Tool.static.name = '';
5914  * Tool group.
5916  * @abstract
5917  * @static
5918  * @inheritable
5919  * @property {string}
5920  */
5921 OO.ui.Tool.static.group = '';
5924  * Tool title.
5926  * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
5927  * is part of a list or menu tool group. If a trigger is associated with an action by the same name
5928  * as the tool, a description of its keyboard shortcut for the appropriate platform will be
5929  * appended to the title if the tool is part of a bar tool group.
5931  * @abstract
5932  * @static
5933  * @inheritable
5934  * @property {string|Function} Title text or a function that returns text
5935  */
5936 OO.ui.Tool.static.title = '';
5939  * Tool can be automatically added to catch-all groups.
5941  * @static
5942  * @inheritable
5943  * @property {boolean}
5944  */
5945 OO.ui.Tool.static.autoAddToCatchall = true;
5948  * Tool can be automatically added to named groups.
5950  * @static
5951  * @property {boolean}
5952  * @inheritable
5953  */
5954 OO.ui.Tool.static.autoAddToGroup = true;
5957  * Check if this tool is compatible with given data.
5959  * @static
5960  * @inheritable
5961  * @param {Mixed} data Data to check
5962  * @return {boolean} Tool can be used with data
5963  */
5964 OO.ui.Tool.static.isCompatibleWith = function () {
5965         return false;
5968 /* Methods */
5971  * Handle the toolbar state being updated.
5973  * This is an abstract method that must be overridden in a concrete subclass.
5975  * @abstract
5976  */
5977 OO.ui.Tool.prototype.onUpdateState = function () {
5978         throw new Error(
5979                 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
5980         );
5984  * Handle the tool being selected.
5986  * This is an abstract method that must be overridden in a concrete subclass.
5988  * @abstract
5989  */
5990 OO.ui.Tool.prototype.onSelect = function () {
5991         throw new Error(
5992                 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
5993         );
5997  * Check if the button is active.
5999  * @return {boolean} Button is active
6000  */
6001 OO.ui.Tool.prototype.isActive = function () {
6002         return this.active;
6006  * Make the button appear active or inactive.
6008  * @param {boolean} state Make button appear active
6009  */
6010 OO.ui.Tool.prototype.setActive = function ( state ) {
6011         this.active = !!state;
6012         if ( this.active ) {
6013                 this.$element.addClass( 'oo-ui-tool-active' );
6014         } else {
6015                 this.$element.removeClass( 'oo-ui-tool-active' );
6016         }
6020  * Get the tool title.
6022  * @param {string|Function} title Title text or a function that returns text
6023  * @chainable
6024  */
6025 OO.ui.Tool.prototype.setTitle = function ( title ) {
6026         this.title = OO.ui.resolveMsg( title );
6027         this.updateTitle();
6028         return this;
6032  * Get the tool title.
6034  * @return {string} Title text
6035  */
6036 OO.ui.Tool.prototype.getTitle = function () {
6037         return this.title;
6041  * Get the tool's symbolic name.
6043  * @return {string} Symbolic name of tool
6044  */
6045 OO.ui.Tool.prototype.getName = function () {
6046         return this.constructor.static.name;
6050  * Update the title.
6051  */
6052 OO.ui.Tool.prototype.updateTitle = function () {
6053         var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
6054                 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
6055                 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
6056                 tooltipParts = [];
6058         this.$title.text( this.title );
6059         this.$accel.text( accel );
6061         if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
6062                 tooltipParts.push( this.title );
6063         }
6064         if ( accelTooltips && typeof accel === 'string' && accel.length ) {
6065                 tooltipParts.push( accel );
6066         }
6067         if ( tooltipParts.length ) {
6068                 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
6069         } else {
6070                 this.$link.removeAttr( 'title' );
6071         }
6075  * Destroy tool.
6076  */
6077 OO.ui.Tool.prototype.destroy = function () {
6078         this.toolbar.disconnect( this );
6079         this.$element.remove();
6083  * Collection of tool groups.
6085  * @class
6086  * @extends OO.ui.Element
6087  * @mixins OO.EventEmitter
6088  * @mixins OO.ui.GroupElement
6090  * @constructor
6091  * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
6092  * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
6093  * @param {Object} [config] Configuration options
6094  * @cfg {boolean} [actions] Add an actions section opposite to the tools
6095  * @cfg {boolean} [shadow] Add a shadow below the toolbar
6096  */
6097 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
6098         // Configuration initialization
6099         config = config || {};
6101         // Parent constructor
6102         OO.ui.Toolbar.super.call( this, config );
6104         // Mixin constructors
6105         OO.EventEmitter.call( this );
6106         OO.ui.GroupElement.call( this, config );
6108         // Properties
6109         this.toolFactory = toolFactory;
6110         this.toolGroupFactory = toolGroupFactory;
6111         this.groups = [];
6112         this.tools = {};
6113         this.$bar = $( '<div>' );
6114         this.$actions = $( '<div>' );
6115         this.initialized = false;
6117         // Events
6118         this.$element
6119                 .add( this.$bar ).add( this.$group ).add( this.$actions )
6120                 .on( 'mousedown touchstart', this.onPointerDown.bind( this ) );
6122         // Initialization
6123         this.$group.addClass( 'oo-ui-toolbar-tools' );
6124         if ( config.actions ) {
6125                 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
6126         }
6127         this.$bar
6128                 .addClass( 'oo-ui-toolbar-bar' )
6129                 .append( this.$group, '<div style="clear:both"></div>' );
6130         if ( config.shadow ) {
6131                 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
6132         }
6133         this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
6136 /* Setup */
6138 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
6139 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
6140 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
6142 /* Methods */
6145  * Get the tool factory.
6147  * @return {OO.ui.ToolFactory} Tool factory
6148  */
6149 OO.ui.Toolbar.prototype.getToolFactory = function () {
6150         return this.toolFactory;
6154  * Get the tool group factory.
6156  * @return {OO.Factory} Tool group factory
6157  */
6158 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
6159         return this.toolGroupFactory;
6163  * Handles mouse down events.
6165  * @param {jQuery.Event} e Mouse down event
6166  */
6167 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
6168         var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
6169                 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
6170         if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
6171                 return false;
6172         }
6176  * Sets up handles and preloads required information for the toolbar to work.
6177  * This must be called after it is attached to a visible document and before doing anything else.
6178  */
6179 OO.ui.Toolbar.prototype.initialize = function () {
6180         this.initialized = true;
6184  * Setup toolbar.
6186  * Tools can be specified in the following ways:
6188  * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6189  * - All tools in a group: `{ group: 'group-name' }`
6190  * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
6192  * @param {Object.<string,Array>} groups List of tool group configurations
6193  * @param {Array|string} [groups.include] Tools to include
6194  * @param {Array|string} [groups.exclude] Tools to exclude
6195  * @param {Array|string} [groups.promote] Tools to promote to the beginning
6196  * @param {Array|string} [groups.demote] Tools to demote to the end
6197  */
6198 OO.ui.Toolbar.prototype.setup = function ( groups ) {
6199         var i, len, type, group,
6200                 items = [],
6201                 defaultType = 'bar';
6203         // Cleanup previous groups
6204         this.reset();
6206         // Build out new groups
6207         for ( i = 0, len = groups.length; i < len; i++ ) {
6208                 group = groups[ i ];
6209                 if ( group.include === '*' ) {
6210                         // Apply defaults to catch-all groups
6211                         if ( group.type === undefined ) {
6212                                 group.type = 'list';
6213                         }
6214                         if ( group.label === undefined ) {
6215                                 group.label = OO.ui.msg( 'ooui-toolbar-more' );
6216                         }
6217                 }
6218                 // Check type has been registered
6219                 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
6220                 items.push(
6221                         this.getToolGroupFactory().create( type, this, group )
6222                 );
6223         }
6224         this.addItems( items );
6228  * Remove all tools and groups from the toolbar.
6229  */
6230 OO.ui.Toolbar.prototype.reset = function () {
6231         var i, len;
6233         this.groups = [];
6234         this.tools = {};
6235         for ( i = 0, len = this.items.length; i < len; i++ ) {
6236                 this.items[ i ].destroy();
6237         }
6238         this.clearItems();
6242  * Destroys toolbar, removing event handlers and DOM elements.
6244  * Call this whenever you are done using a toolbar.
6245  */
6246 OO.ui.Toolbar.prototype.destroy = function () {
6247         this.reset();
6248         this.$element.remove();
6252  * Check if tool has not been used yet.
6254  * @param {string} name Symbolic name of tool
6255  * @return {boolean} Tool is available
6256  */
6257 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
6258         return !this.tools[ name ];
6262  * Prevent tool from being used again.
6264  * @param {OO.ui.Tool} tool Tool to reserve
6265  */
6266 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
6267         this.tools[ tool.getName() ] = tool;
6271  * Allow tool to be used again.
6273  * @param {OO.ui.Tool} tool Tool to release
6274  */
6275 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
6276         delete this.tools[ tool.getName() ];
6280  * Get accelerator label for tool.
6282  * This is a stub that should be overridden to provide access to accelerator information.
6284  * @param {string} name Symbolic name of tool
6285  * @return {string|undefined} Tool accelerator label if available
6286  */
6287 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
6288         return undefined;
6292  * Collection of tools.
6294  * Tools can be specified in the following ways:
6296  * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6297  * - All tools in a group: `{ group: 'group-name' }`
6298  * - All tools: `'*'`
6300  * @abstract
6301  * @class
6302  * @extends OO.ui.Widget
6303  * @mixins OO.ui.GroupElement
6305  * @constructor
6306  * @param {OO.ui.Toolbar} toolbar
6307  * @param {Object} [config] Configuration options
6308  * @cfg {Array|string} [include=[]] List of tools to include
6309  * @cfg {Array|string} [exclude=[]] List of tools to exclude
6310  * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
6311  * @cfg {Array|string} [demote=[]] List of tools to demote to the end
6312  */
6313 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
6314         // Configuration initialization
6315         config = config || {};
6317         // Parent constructor
6318         OO.ui.ToolGroup.super.call( this, config );
6320         // Mixin constructors
6321         OO.ui.GroupElement.call( this, config );
6323         // Properties
6324         this.toolbar = toolbar;
6325         this.tools = {};
6326         this.pressed = null;
6327         this.autoDisabled = false;
6328         this.include = config.include || [];
6329         this.exclude = config.exclude || [];
6330         this.promote = config.promote || [];
6331         this.demote = config.demote || [];
6332         this.onCapturedMouseUpHandler = this.onCapturedMouseUp.bind( this );
6334         // Events
6335         this.$element.on( {
6336                 'mousedown touchstart': this.onPointerDown.bind( this ),
6337                 'mouseup touchend': this.onPointerUp.bind( this ),
6338                 mouseover: this.onMouseOver.bind( this ),
6339                 mouseout: this.onMouseOut.bind( this )
6340         } );
6341         this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
6342         this.aggregate( { disable: 'itemDisable' } );
6343         this.connect( this, { itemDisable: 'updateDisabled' } );
6345         // Initialization
6346         this.$group.addClass( 'oo-ui-toolGroup-tools' );
6347         this.$element
6348                 .addClass( 'oo-ui-toolGroup' )
6349                 .append( this.$group );
6350         this.populate();
6353 /* Setup */
6355 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
6356 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
6358 /* Events */
6361  * @event update
6362  */
6364 /* Static Properties */
6367  * Show labels in tooltips.
6369  * @static
6370  * @inheritable
6371  * @property {boolean}
6372  */
6373 OO.ui.ToolGroup.static.titleTooltips = false;
6376  * Show acceleration labels in tooltips.
6378  * @static
6379  * @inheritable
6380  * @property {boolean}
6381  */
6382 OO.ui.ToolGroup.static.accelTooltips = false;
6385  * Automatically disable the toolgroup when all tools are disabled
6387  * @static
6388  * @inheritable
6389  * @property {boolean}
6390  */
6391 OO.ui.ToolGroup.static.autoDisable = true;
6393 /* Methods */
6396  * @inheritdoc
6397  */
6398 OO.ui.ToolGroup.prototype.isDisabled = function () {
6399         return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
6403  * @inheritdoc
6404  */
6405 OO.ui.ToolGroup.prototype.updateDisabled = function () {
6406         var i, item, allDisabled = true;
6408         if ( this.constructor.static.autoDisable ) {
6409                 for ( i = this.items.length - 1; i >= 0; i-- ) {
6410                         item = this.items[ i ];
6411                         if ( !item.isDisabled() ) {
6412                                 allDisabled = false;
6413                                 break;
6414                         }
6415                 }
6416                 this.autoDisabled = allDisabled;
6417         }
6418         OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
6422  * Handle mouse down events.
6424  * @param {jQuery.Event} e Mouse down event
6425  */
6426 OO.ui.ToolGroup.prototype.onPointerDown = function ( e ) {
6427         // e.which is 0 for touch events, 1 for left mouse button
6428         if ( !this.isDisabled() && e.which <= 1 ) {
6429                 this.pressed = this.getTargetTool( e );
6430                 if ( this.pressed ) {
6431                         this.pressed.setActive( true );
6432                         this.getElementDocument().addEventListener(
6433                                 'mouseup', this.onCapturedMouseUpHandler, true
6434                         );
6435                 }
6436         }
6437         return false;
6441  * Handle captured mouse up events.
6443  * @param {Event} e Mouse up event
6444  */
6445 OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
6446         this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
6447         // onPointerUp may be called a second time, depending on where the mouse is when the button is
6448         // released, but since `this.pressed` will no longer be true, the second call will be ignored.
6449         this.onPointerUp( e );
6453  * Handle mouse up events.
6455  * @param {jQuery.Event} e Mouse up event
6456  */
6457 OO.ui.ToolGroup.prototype.onPointerUp = function ( e ) {
6458         var tool = this.getTargetTool( e );
6460         // e.which is 0 for touch events, 1 for left mouse button
6461         if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === tool ) {
6462                 this.pressed.onSelect();
6463         }
6465         this.pressed = null;
6466         return false;
6470  * Handle mouse over events.
6472  * @param {jQuery.Event} e Mouse over event
6473  */
6474 OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
6475         var tool = this.getTargetTool( e );
6477         if ( this.pressed && this.pressed === tool ) {
6478                 this.pressed.setActive( true );
6479         }
6483  * Handle mouse out events.
6485  * @param {jQuery.Event} e Mouse out event
6486  */
6487 OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
6488         var tool = this.getTargetTool( e );
6490         if ( this.pressed && this.pressed === tool ) {
6491                 this.pressed.setActive( false );
6492         }
6496  * Get the closest tool to a jQuery.Event.
6498  * Only tool links are considered, which prevents other elements in the tool such as popups from
6499  * triggering tool group interactions.
6501  * @private
6502  * @param {jQuery.Event} e
6503  * @return {OO.ui.Tool|null} Tool, `null` if none was found
6504  */
6505 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
6506         var tool,
6507                 $item = $( e.target ).closest( '.oo-ui-tool-link' );
6509         if ( $item.length ) {
6510                 tool = $item.parent().data( 'oo-ui-tool' );
6511         }
6513         return tool && !tool.isDisabled() ? tool : null;
6517  * Handle tool registry register events.
6519  * If a tool is registered after the group is created, we must repopulate the list to account for:
6521  * - a tool being added that may be included
6522  * - a tool already included being overridden
6524  * @param {string} name Symbolic name of tool
6525  */
6526 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
6527         this.populate();
6531  * Get the toolbar this group is in.
6533  * @return {OO.ui.Toolbar} Toolbar of group
6534  */
6535 OO.ui.ToolGroup.prototype.getToolbar = function () {
6536         return this.toolbar;
6540  * Add and remove tools based on configuration.
6541  */
6542 OO.ui.ToolGroup.prototype.populate = function () {
6543         var i, len, name, tool,
6544                 toolFactory = this.toolbar.getToolFactory(),
6545                 names = {},
6546                 add = [],
6547                 remove = [],
6548                 list = this.toolbar.getToolFactory().getTools(
6549                         this.include, this.exclude, this.promote, this.demote
6550                 );
6552         // Build a list of needed tools
6553         for ( i = 0, len = list.length; i < len; i++ ) {
6554                 name = list[ i ];
6555                 if (
6556                         // Tool exists
6557                         toolFactory.lookup( name ) &&
6558                         // Tool is available or is already in this group
6559                         ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
6560                 ) {
6561                         tool = this.tools[ name ];
6562                         if ( !tool ) {
6563                                 // Auto-initialize tools on first use
6564                                 this.tools[ name ] = tool = toolFactory.create( name, this );
6565                                 tool.updateTitle();
6566                         }
6567                         this.toolbar.reserveTool( tool );
6568                         add.push( tool );
6569                         names[ name ] = true;
6570                 }
6571         }
6572         // Remove tools that are no longer needed
6573         for ( name in this.tools ) {
6574                 if ( !names[ name ] ) {
6575                         this.tools[ name ].destroy();
6576                         this.toolbar.releaseTool( this.tools[ name ] );
6577                         remove.push( this.tools[ name ] );
6578                         delete this.tools[ name ];
6579                 }
6580         }
6581         if ( remove.length ) {
6582                 this.removeItems( remove );
6583         }
6584         // Update emptiness state
6585         if ( add.length ) {
6586                 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
6587         } else {
6588                 this.$element.addClass( 'oo-ui-toolGroup-empty' );
6589         }
6590         // Re-add tools (moving existing ones to new locations)
6591         this.addItems( add );
6592         // Disabled state may depend on items
6593         this.updateDisabled();
6597  * Destroy tool group.
6598  */
6599 OO.ui.ToolGroup.prototype.destroy = function () {
6600         var name;
6602         this.clearItems();
6603         this.toolbar.getToolFactory().disconnect( this );
6604         for ( name in this.tools ) {
6605                 this.toolbar.releaseTool( this.tools[ name ] );
6606                 this.tools[ name ].disconnect( this ).destroy();
6607                 delete this.tools[ name ];
6608         }
6609         this.$element.remove();
6613  * Dialog for showing a message.
6615  * User interface:
6616  * - Registers two actions by default (safe and primary).
6617  * - Renders action widgets in the footer.
6619  * @class
6620  * @extends OO.ui.Dialog
6622  * @constructor
6623  * @param {Object} [config] Configuration options
6624  */
6625 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
6626         // Parent constructor
6627         OO.ui.MessageDialog.super.call( this, config );
6629         // Properties
6630         this.verticalActionLayout = null;
6632         // Initialization
6633         this.$element.addClass( 'oo-ui-messageDialog' );
6636 /* Inheritance */
6638 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
6640 /* Static Properties */
6642 OO.ui.MessageDialog.static.name = 'message';
6644 OO.ui.MessageDialog.static.size = 'small';
6646 OO.ui.MessageDialog.static.verbose = false;
6649  * Dialog title.
6651  * A confirmation dialog's title should describe what the progressive action will do. An alert
6652  * dialog's title should describe what event occurred.
6654  * @static
6655  * inheritable
6656  * @property {jQuery|string|Function|null}
6657  */
6658 OO.ui.MessageDialog.static.title = null;
6661  * A confirmation dialog's message should describe the consequences of the progressive action. An
6662  * alert dialog's message should describe why the event occurred.
6664  * @static
6665  * inheritable
6666  * @property {jQuery|string|Function|null}
6667  */
6668 OO.ui.MessageDialog.static.message = null;
6670 OO.ui.MessageDialog.static.actions = [
6671         { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
6672         { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
6675 /* Methods */
6678  * @inheritdoc
6679  */
6680 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
6681         OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
6683         // Events
6684         this.manager.connect( this, {
6685                 resize: 'onResize'
6686         } );
6688         return this;
6692  * @inheritdoc
6693  */
6694 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
6695         this.fitActions();
6696         return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
6700  * Handle window resized events.
6701  */
6702 OO.ui.MessageDialog.prototype.onResize = function () {
6703         var dialog = this;
6704         dialog.fitActions();
6705         // Wait for CSS transition to finish and do it again :(
6706         setTimeout( function () {
6707                 dialog.fitActions();
6708         }, 300 );
6712  * Toggle action layout between vertical and horizontal.
6714  * @param {boolean} [value] Layout actions vertically, omit to toggle
6715  * @chainable
6716  */
6717 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
6718         value = value === undefined ? !this.verticalActionLayout : !!value;
6720         if ( value !== this.verticalActionLayout ) {
6721                 this.verticalActionLayout = value;
6722                 this.$actions
6723                         .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
6724                         .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
6725         }
6727         return this;
6731  * @inheritdoc
6732  */
6733 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
6734         if ( action ) {
6735                 return new OO.ui.Process( function () {
6736                         this.close( { action: action } );
6737                 }, this );
6738         }
6739         return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
6743  * @inheritdoc
6745  * @param {Object} [data] Dialog opening data
6746  * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
6747  * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
6748  * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
6749  * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
6750  *   action item
6751  */
6752 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
6753         data = data || {};
6755         // Parent method
6756         return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
6757                 .next( function () {
6758                         this.title.setLabel(
6759                                 data.title !== undefined ? data.title : this.constructor.static.title
6760                         );
6761                         this.message.setLabel(
6762                                 data.message !== undefined ? data.message : this.constructor.static.message
6763                         );
6764                         this.message.$element.toggleClass(
6765                                 'oo-ui-messageDialog-message-verbose',
6766                                 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
6767                         );
6768                 }, this );
6772  * @inheritdoc
6773  */
6774 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
6775         var bodyHeight, oldOverflow,
6776                 $scrollable = this.container.$element;
6778         oldOverflow = $scrollable[ 0 ].style.overflow;
6779         $scrollable[ 0 ].style.overflow = 'hidden';
6781         OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
6783         bodyHeight = this.text.$element.outerHeight( true );
6784         $scrollable[ 0 ].style.overflow = oldOverflow;
6786         return bodyHeight;
6790  * @inheritdoc
6791  */
6792 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
6793         var $scrollable = this.container.$element;
6794         OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
6796         // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
6797         // Need to do it after transition completes (250ms), add 50ms just in case.
6798         setTimeout( function () {
6799                 var oldOverflow = $scrollable[ 0 ].style.overflow;
6800                 $scrollable[ 0 ].style.overflow = 'hidden';
6802                 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
6804                 $scrollable[ 0 ].style.overflow = oldOverflow;
6805         }, 300 );
6807         return this;
6811  * @inheritdoc
6812  */
6813 OO.ui.MessageDialog.prototype.initialize = function () {
6814         // Parent method
6815         OO.ui.MessageDialog.super.prototype.initialize.call( this );
6817         // Properties
6818         this.$actions = $( '<div>' );
6819         this.container = new OO.ui.PanelLayout( {
6820                 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
6821         } );
6822         this.text = new OO.ui.PanelLayout( {
6823                 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
6824         } );
6825         this.message = new OO.ui.LabelWidget( {
6826                 classes: [ 'oo-ui-messageDialog-message' ]
6827         } );
6829         // Initialization
6830         this.title.$element.addClass( 'oo-ui-messageDialog-title' );
6831         this.$content.addClass( 'oo-ui-messageDialog-content' );
6832         this.container.$element.append( this.text.$element );
6833         this.text.$element.append( this.title.$element, this.message.$element );
6834         this.$body.append( this.container.$element );
6835         this.$actions.addClass( 'oo-ui-messageDialog-actions' );
6836         this.$foot.append( this.$actions );
6840  * @inheritdoc
6841  */
6842 OO.ui.MessageDialog.prototype.attachActions = function () {
6843         var i, len, other, special, others;
6845         // Parent method
6846         OO.ui.MessageDialog.super.prototype.attachActions.call( this );
6848         special = this.actions.getSpecial();
6849         others = this.actions.getOthers();
6850         if ( special.safe ) {
6851                 this.$actions.append( special.safe.$element );
6852                 special.safe.toggleFramed( false );
6853         }
6854         if ( others.length ) {
6855                 for ( i = 0, len = others.length; i < len; i++ ) {
6856                         other = others[ i ];
6857                         this.$actions.append( other.$element );
6858                         other.toggleFramed( false );
6859                 }
6860         }
6861         if ( special.primary ) {
6862                 this.$actions.append( special.primary.$element );
6863                 special.primary.toggleFramed( false );
6864         }
6866         if ( !this.isOpening() ) {
6867                 // If the dialog is currently opening, this will be called automatically soon.
6868                 // This also calls #fitActions.
6869                 this.updateSize();
6870         }
6874  * Fit action actions into columns or rows.
6876  * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
6877  */
6878 OO.ui.MessageDialog.prototype.fitActions = function () {
6879         var i, len, action,
6880                 previous = this.verticalActionLayout,
6881                 actions = this.actions.get();
6883         // Detect clipping
6884         this.toggleVerticalActionLayout( false );
6885         for ( i = 0, len = actions.length; i < len; i++ ) {
6886                 action = actions[ i ];
6887                 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
6888                         this.toggleVerticalActionLayout( true );
6889                         break;
6890                 }
6891         }
6893         // Move the body out of the way of the foot
6894         this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
6896         if ( this.verticalActionLayout !== previous ) {
6897                 // We changed the layout, window height might need to be updated.
6898                 this.updateSize();
6899         }
6903  * Navigation dialog window.
6905  * Logic:
6906  * - Show and hide errors.
6907  * - Retry an action.
6909  * User interface:
6910  * - Renders header with dialog title and one action widget on either side
6911  *   (a 'safe' button on the left, and a 'primary' button on the right, both of
6912  *   which close the dialog).
6913  * - Displays any action widgets in the footer (none by default).
6914  * - Ability to dismiss errors.
6916  * Subclass responsibilities:
6917  * - Register a 'safe' action.
6918  * - Register a 'primary' action.
6919  * - Add content to the dialog.
6921  * @abstract
6922  * @class
6923  * @extends OO.ui.Dialog
6925  * @constructor
6926  * @param {Object} [config] Configuration options
6927  */
6928 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
6929         // Parent constructor
6930         OO.ui.ProcessDialog.super.call( this, config );
6932         // Initialization
6933         this.$element.addClass( 'oo-ui-processDialog' );
6936 /* Setup */
6938 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
6940 /* Methods */
6943  * Handle dismiss button click events.
6945  * Hides errors.
6946  */
6947 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
6948         this.hideErrors();
6952  * Handle retry button click events.
6954  * Hides errors and then tries again.
6955  */
6956 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
6957         this.hideErrors();
6958         this.executeAction( this.currentAction.getAction() );
6962  * @inheritdoc
6963  */
6964 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
6965         if ( this.actions.isSpecial( action ) ) {
6966                 this.fitLabel();
6967         }
6968         return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
6972  * @inheritdoc
6973  */
6974 OO.ui.ProcessDialog.prototype.initialize = function () {
6975         // Parent method
6976         OO.ui.ProcessDialog.super.prototype.initialize.call( this );
6978         // Properties
6979         this.$navigation = $( '<div>' );
6980         this.$location = $( '<div>' );
6981         this.$safeActions = $( '<div>' );
6982         this.$primaryActions = $( '<div>' );
6983         this.$otherActions = $( '<div>' );
6984         this.dismissButton = new OO.ui.ButtonWidget( {
6985                 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
6986         } );
6987         this.retryButton = new OO.ui.ButtonWidget();
6988         this.$errors = $( '<div>' );
6989         this.$errorsTitle = $( '<div>' );
6991         // Events
6992         this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
6993         this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
6995         // Initialization
6996         this.title.$element.addClass( 'oo-ui-processDialog-title' );
6997         this.$location
6998                 .append( this.title.$element )
6999                 .addClass( 'oo-ui-processDialog-location' );
7000         this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
7001         this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
7002         this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
7003         this.$errorsTitle
7004                 .addClass( 'oo-ui-processDialog-errors-title' )
7005                 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
7006         this.$errors
7007                 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
7008                 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
7009         this.$content
7010                 .addClass( 'oo-ui-processDialog-content' )
7011                 .append( this.$errors );
7012         this.$navigation
7013                 .addClass( 'oo-ui-processDialog-navigation' )
7014                 .append( this.$safeActions, this.$location, this.$primaryActions );
7015         this.$head.append( this.$navigation );
7016         this.$foot.append( this.$otherActions );
7020  * @inheritdoc
7021  */
7022 OO.ui.ProcessDialog.prototype.attachActions = function () {
7023         var i, len, other, special, others;
7025         // Parent method
7026         OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
7028         special = this.actions.getSpecial();
7029         others = this.actions.getOthers();
7030         if ( special.primary ) {
7031                 this.$primaryActions.append( special.primary.$element );
7032                 special.primary.toggleFramed( true );
7033         }
7034         if ( others.length ) {
7035                 for ( i = 0, len = others.length; i < len; i++ ) {
7036                         other = others[ i ];
7037                         this.$otherActions.append( other.$element );
7038                         other.toggleFramed( true );
7039                 }
7040         }
7041         if ( special.safe ) {
7042                 this.$safeActions.append( special.safe.$element );
7043                 special.safe.toggleFramed( true );
7044         }
7046         this.fitLabel();
7047         this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
7051  * @inheritdoc
7052  */
7053 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
7054         OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
7055                 .fail( this.showErrors.bind( this ) );
7059  * Fit label between actions.
7061  * @chainable
7062  */
7063 OO.ui.ProcessDialog.prototype.fitLabel = function () {
7064         var width = Math.max(
7065                 this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
7066                 this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
7067         );
7068         this.$location.css( { paddingLeft: width, paddingRight: width } );
7070         return this;
7074  * Handle errors that occurred during accept or reject processes.
7076  * @param {OO.ui.Error[]} errors Errors to be handled
7077  */
7078 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
7079         var i, len, $item,
7080                 items = [],
7081                 recoverable = true,
7082                 warning = false;
7084         for ( i = 0, len = errors.length; i < len; i++ ) {
7085                 if ( !errors[ i ].isRecoverable() ) {
7086                         recoverable = false;
7087                 }
7088                 if ( errors[ i ].isWarning() ) {
7089                         warning = true;
7090                 }
7091                 $item = $( '<div>' )
7092                         .addClass( 'oo-ui-processDialog-error' )
7093                         .append( errors[ i ].getMessage() );
7094                 items.push( $item[ 0 ] );
7095         }
7096         this.$errorItems = $( items );
7097         if ( recoverable ) {
7098                 this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() );
7099         } else {
7100                 this.currentAction.setDisabled( true );
7101         }
7102         if ( warning ) {
7103                 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
7104         } else {
7105                 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
7106         }
7107         this.retryButton.toggle( recoverable );
7108         this.$errorsTitle.after( this.$errorItems );
7109         this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
7113  * Hide errors.
7114  */
7115 OO.ui.ProcessDialog.prototype.hideErrors = function () {
7116         this.$errors.addClass( 'oo-ui-element-hidden' );
7117         this.$errorItems.remove();
7118         this.$errorItems = null;
7122  * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
7123  * which is a widget that is specified by reference before any optional configuration settings.
7125  * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
7127  * - **left**: The label is placed before the field-widget and aligned with the left margin.
7128  *             A left-alignment is used for forms with many fields.
7129  * - **right**: The label is placed before the field-widget and aligned to the right margin.
7130  *              A right-alignment is used for long but familiar forms which users tab through,
7131  *              verifying the current field with a quick glance at the label.
7132  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
7133  *            that users fill out from top to bottom.
7134  * - **inline**: The label is placed after the field-widget and aligned to the left.
7135                  An inline-alignment is best used with checkboxes or radio buttons.
7137  * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
7138  * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
7140  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
7141  * @class
7142  * @extends OO.ui.Layout
7143  * @mixins OO.ui.LabelElement
7145  * @constructor
7146  * @param {OO.ui.Widget} fieldWidget Field widget
7147  * @param {Object} [config] Configuration options
7148  * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7149  * @cfg {string} [help] Explanatory text shown as a '?' icon.
7150  */
7151 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
7152         var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
7154         // Configuration initialization
7155         config = $.extend( { align: 'left' }, config );
7157         // Parent constructor
7158         OO.ui.FieldLayout.super.call( this, config );
7160         // Mixin constructors
7161         OO.ui.LabelElement.call( this, config );
7163         // Properties
7164         this.fieldWidget = fieldWidget;
7165         this.$field = $( '<div>' );
7166         this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
7167         this.align = null;
7168         if ( config.help ) {
7169                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7170                         classes: [ 'oo-ui-fieldLayout-help' ],
7171                         framed: false,
7172                         icon: 'info'
7173                 } );
7175                 this.popupButtonWidget.getPopup().$body.append(
7176                         $( '<div>' )
7177                                 .text( config.help )
7178                                 .addClass( 'oo-ui-fieldLayout-help-content' )
7179                 );
7180                 this.$help = this.popupButtonWidget.$element;
7181         } else {
7182                 this.$help = $( [] );
7183         }
7185         // Events
7186         if ( hasInputWidget ) {
7187                 this.$label.on( 'click', this.onLabelClick.bind( this ) );
7188         }
7189         this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
7191         // Initialization
7192         this.$element
7193                 .addClass( 'oo-ui-fieldLayout' )
7194                 .append( this.$help, this.$body );
7195         this.$body.addClass( 'oo-ui-fieldLayout-body' );
7196         this.$field
7197                 .addClass( 'oo-ui-fieldLayout-field' )
7198                 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
7199                 .append( this.fieldWidget.$element );
7201         this.setAlignment( config.align );
7204 /* Setup */
7206 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
7207 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
7209 /* Methods */
7212  * Handle field disable events.
7214  * @param {boolean} value Field is disabled
7215  */
7216 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
7217         this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
7221  * Handle label mouse click events.
7223  * @param {jQuery.Event} e Mouse click event
7224  */
7225 OO.ui.FieldLayout.prototype.onLabelClick = function () {
7226         this.fieldWidget.simulateLabelClick();
7227         return false;
7231  * Get the field.
7233  * @return {OO.ui.Widget} Field widget
7234  */
7235 OO.ui.FieldLayout.prototype.getField = function () {
7236         return this.fieldWidget;
7240  * Set the field alignment mode.
7242  * @private
7243  * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
7244  * @chainable
7245  */
7246 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
7247         if ( value !== this.align ) {
7248                 // Default to 'left'
7249                 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
7250                         value = 'left';
7251                 }
7252                 // Reorder elements
7253                 if ( value === 'inline' ) {
7254                         this.$body.append( this.$field, this.$label );
7255                 } else {
7256                         this.$body.append( this.$label, this.$field );
7257                 }
7258                 // Set classes. The following classes can be used here:
7259                 // * oo-ui-fieldLayout-align-left
7260                 // * oo-ui-fieldLayout-align-right
7261                 // * oo-ui-fieldLayout-align-top
7262                 // * oo-ui-fieldLayout-align-inline
7263                 if ( this.align ) {
7264                         this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
7265                 }
7266                 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
7267                 this.align = value;
7268         }
7270         return this;
7274  * Layout made of a field, a button, and an optional label.
7276  * @class
7277  * @extends OO.ui.FieldLayout
7279  * @constructor
7280  * @param {OO.ui.Widget} fieldWidget Field widget
7281  * @param {OO.ui.ButtonWidget} buttonWidget Button widget
7282  * @param {Object} [config] Configuration options
7283  * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7284  * @cfg {string} [help] Explanatory text shown as a '?' icon.
7285  */
7286 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
7287         // Configuration initialization
7288         config = $.extend( { align: 'left' }, config );
7290         // Parent constructor
7291         OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
7293         // Mixin constructors
7294         OO.ui.LabelElement.call( this, config );
7296         // Properties
7297         this.fieldWidget = fieldWidget;
7298         this.buttonWidget = buttonWidget;
7299         this.$button = $( '<div>' )
7300                 .addClass( 'oo-ui-actionFieldLayout-button' )
7301                 .append( this.buttonWidget.$element );
7302         this.$input = $( '<div>' )
7303                 .addClass( 'oo-ui-actionFieldLayout-input' )
7304                 .append( this.fieldWidget.$element );
7305         this.$field
7306                 .addClass( 'oo-ui-actionFieldLayout' )
7307                 .append( this.$input, this.$button );
7310 /* Setup */
7312 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
7315  * Layout made of a fieldset and optional legend.
7317  * Just add OO.ui.FieldLayout items.
7319  * @class
7320  * @extends OO.ui.Layout
7321  * @mixins OO.ui.IconElement
7322  * @mixins OO.ui.LabelElement
7323  * @mixins OO.ui.GroupElement
7325  * @constructor
7326  * @param {Object} [config] Configuration options
7327  * @cfg {OO.ui.FieldLayout[]} [items] Items to add
7328  */
7329 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
7330         // Configuration initialization
7331         config = config || {};
7333         // Parent constructor
7334         OO.ui.FieldsetLayout.super.call( this, config );
7336         // Mixin constructors
7337         OO.ui.IconElement.call( this, config );
7338         OO.ui.LabelElement.call( this, config );
7339         OO.ui.GroupElement.call( this, config );
7341         if ( config.help ) {
7342                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7343                         classes: [ 'oo-ui-fieldsetLayout-help' ],
7344                         framed: false,
7345                         icon: 'info'
7346                 } );
7348                 this.popupButtonWidget.getPopup().$body.append(
7349                         $( '<div>' )
7350                                 .text( config.help )
7351                                 .addClass( 'oo-ui-fieldsetLayout-help-content' )
7352                 );
7353                 this.$help = this.popupButtonWidget.$element;
7354         } else {
7355                 this.$help = $( [] );
7356         }
7358         // Initialization
7359         this.$element
7360                 .addClass( 'oo-ui-fieldsetLayout' )
7361                 .prepend( this.$help, this.$icon, this.$label, this.$group );
7362         if ( Array.isArray( config.items ) ) {
7363                 this.addItems( config.items );
7364         }
7367 /* Setup */
7369 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
7370 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
7371 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
7372 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
7375  * Layout with an HTML form.
7377  * @class
7378  * @extends OO.ui.Layout
7380  * @constructor
7381  * @param {Object} [config] Configuration options
7382  * @cfg {string} [method] HTML form `method` attribute
7383  * @cfg {string} [action] HTML form `action` attribute
7384  * @cfg {string} [enctype] HTML form `enctype` attribute
7385  */
7386 OO.ui.FormLayout = function OoUiFormLayout( config ) {
7387         // Configuration initialization
7388         config = config || {};
7390         // Parent constructor
7391         OO.ui.FormLayout.super.call( this, config );
7393         // Events
7394         this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
7396         // Initialization
7397         this.$element
7398                 .addClass( 'oo-ui-formLayout' )
7399                 .attr( {
7400                         method: config.method,
7401                         action: config.action,
7402                         enctype: config.enctype
7403                 } );
7406 /* Setup */
7408 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
7410 /* Events */
7413  * @event submit
7414  */
7416 /* Static Properties */
7418 OO.ui.FormLayout.static.tagName = 'form';
7420 /* Methods */
7423  * Handle form submit events.
7425  * @param {jQuery.Event} e Submit event
7426  * @fires submit
7427  */
7428 OO.ui.FormLayout.prototype.onFormSubmit = function () {
7429         this.emit( 'submit' );
7430         return false;
7434  * Layout made of proportionally sized columns and rows.
7436  * @class
7437  * @extends OO.ui.Layout
7438  * @deprecated Use OO.ui.MenuLayout or plain CSS instead.
7440  * @constructor
7441  * @param {OO.ui.PanelLayout[]} panels Panels in the grid
7442  * @param {Object} [config] Configuration options
7443  * @cfg {number[]} [widths] Widths of columns as ratios
7444  * @cfg {number[]} [heights] Heights of rows as ratios
7445  */
7446 OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
7447         var i, len, widths;
7449         // Configuration initialization
7450         config = config || {};
7452         // Parent constructor
7453         OO.ui.GridLayout.super.call( this, config );
7455         // Properties
7456         this.panels = [];
7457         this.widths = [];
7458         this.heights = [];
7460         // Initialization
7461         this.$element.addClass( 'oo-ui-gridLayout' );
7462         for ( i = 0, len = panels.length; i < len; i++ ) {
7463                 this.panels.push( panels[ i ] );
7464                 this.$element.append( panels[ i ].$element );
7465         }
7466         if ( config.widths || config.heights ) {
7467                 this.layout( config.widths || [ 1 ], config.heights || [ 1 ] );
7468         } else {
7469                 // Arrange in columns by default
7470                 widths = this.panels.map( function () { return 1; } );
7471                 this.layout( widths, [ 1 ] );
7472         }
7475 /* Setup */
7477 OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
7479 /* Events */
7482  * @event layout
7483  */
7486  * @event update
7487  */
7489 /* Methods */
7492  * Set grid dimensions.
7494  * @param {number[]} widths Widths of columns as ratios
7495  * @param {number[]} heights Heights of rows as ratios
7496  * @fires layout
7497  * @throws {Error} If grid is not large enough to fit all panels
7498  */
7499 OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
7500         var x, y,
7501                 xd = 0,
7502                 yd = 0,
7503                 cols = widths.length,
7504                 rows = heights.length;
7506         // Verify grid is big enough to fit panels
7507         if ( cols * rows < this.panels.length ) {
7508                 throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
7509         }
7511         // Sum up denominators
7512         for ( x = 0; x < cols; x++ ) {
7513                 xd += widths[ x ];
7514         }
7515         for ( y = 0; y < rows; y++ ) {
7516                 yd += heights[ y ];
7517         }
7518         // Store factors
7519         this.widths = [];
7520         this.heights = [];
7521         for ( x = 0; x < cols; x++ ) {
7522                 this.widths[ x ] = widths[ x ] / xd;
7523         }
7524         for ( y = 0; y < rows; y++ ) {
7525                 this.heights[ y ] = heights[ y ] / yd;
7526         }
7527         // Synchronize view
7528         this.update();
7529         this.emit( 'layout' );
7533  * Update panel positions and sizes.
7535  * @fires update
7536  */
7537 OO.ui.GridLayout.prototype.update = function () {
7538         var x, y, panel, width, height, dimensions,
7539                 i = 0,
7540                 top = 0,
7541                 left = 0,
7542                 cols = this.widths.length,
7543                 rows = this.heights.length;
7545         for ( y = 0; y < rows; y++ ) {
7546                 height = this.heights[ y ];
7547                 for ( x = 0; x < cols; x++ ) {
7548                         width = this.widths[ x ];
7549                         panel = this.panels[ i ];
7550                         dimensions = {
7551                                 width: ( width * 100 ) + '%',
7552                                 height: ( height * 100 ) + '%',
7553                                 top: ( top * 100 ) + '%'
7554                         };
7555                         // If RTL, reverse:
7556                         if ( OO.ui.Element.static.getDir( document ) === 'rtl' ) {
7557                                 dimensions.right = ( left * 100 ) + '%';
7558                         } else {
7559                                 dimensions.left = ( left * 100 ) + '%';
7560                         }
7561                         // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
7562                         if ( width === 0 || height === 0 ) {
7563                                 dimensions.visibility = 'hidden';
7564                         } else {
7565                                 dimensions.visibility = '';
7566                         }
7567                         panel.$element.css( dimensions );
7568                         i++;
7569                         left += width;
7570                 }
7571                 top += height;
7572                 left = 0;
7573         }
7575         this.emit( 'update' );
7579  * Get a panel at a given position.
7581  * The x and y position is affected by the current grid layout.
7583  * @param {number} x Horizontal position
7584  * @param {number} y Vertical position
7585  * @return {OO.ui.PanelLayout} The panel at the given position
7586  */
7587 OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
7588         return this.panels[ ( x * this.widths.length ) + y ];
7592  * Layout with a content and menu area.
7594  * The menu area can be positioned at the top, after, bottom or before. The content area will fill
7595  * all remaining space.
7597  * @class
7598  * @extends OO.ui.Layout
7600  * @constructor
7601  * @param {Object} [config] Configuration options
7602  * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit
7603  * @cfg {boolean} [showMenu=true] Show menu
7604  * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before`
7605  * @cfg {boolean} [collapse] Collapse the menu out of view
7606  */
7607 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
7608         var positions = this.constructor.static.menuPositions;
7610         // Configuration initialization
7611         config = config || {};
7613         // Parent constructor
7614         OO.ui.MenuLayout.super.call( this, config );
7616         // Properties
7617         this.showMenu = config.showMenu !== false;
7618         this.menuSize = config.menuSize || '18em';
7619         this.menuPosition = positions[ config.menuPosition ] || positions.before;
7621         /**
7622          * Menu DOM node
7623          *
7624          * @property {jQuery}
7625          */
7626         this.$menu = $( '<div>' );
7627         /**
7628          * Content DOM node
7629          *
7630          * @property {jQuery}
7631          */
7632         this.$content = $( '<div>' );
7634         // Initialization
7635         this.toggleMenu( this.showMenu );
7636         this.updateSizes();
7637         this.$menu
7638                 .addClass( 'oo-ui-menuLayout-menu' )
7639                 .css( this.menuPosition.sizeProperty, this.menuSize );
7640         this.$content.addClass( 'oo-ui-menuLayout-content' );
7641         this.$element
7642                 .addClass( 'oo-ui-menuLayout ' + this.menuPosition.className )
7643                 .append( this.$content, this.$menu );
7646 /* Setup */
7648 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
7650 /* Static Properties */
7652 OO.ui.MenuLayout.static.menuPositions = {
7653         top: {
7654                 sizeProperty: 'height',
7655                 className: 'oo-ui-menuLayout-top'
7656         },
7657         after: {
7658                 sizeProperty: 'width',
7659                 className: 'oo-ui-menuLayout-after'
7660         },
7661         bottom: {
7662                 sizeProperty: 'height',
7663                 className: 'oo-ui-menuLayout-bottom'
7664         },
7665         before: {
7666                 sizeProperty: 'width',
7667                 className: 'oo-ui-menuLayout-before'
7668         }
7671 /* Methods */
7674  * Toggle menu.
7676  * @param {boolean} showMenu Show menu, omit to toggle
7677  * @chainable
7678  */
7679 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
7680         showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
7682         if ( this.showMenu !== showMenu ) {
7683                 this.showMenu = showMenu;
7684                 this.updateSizes();
7685         }
7687         return this;
7691  * Check if menu is visible
7693  * @return {boolean} Menu is visible
7694  */
7695 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
7696         return this.showMenu;
7700  * Set menu size.
7702  * @param {number|string} size Size of menu in pixels or any CSS unit
7703  * @chainable
7704  */
7705 OO.ui.MenuLayout.prototype.setMenuSize = function ( size ) {
7706         this.menuSize = size;
7707         this.updateSizes();
7709         return this;
7713  * Update menu and content CSS based on current menu size and visibility
7715  * This method is called internally when size or position is changed.
7716  */
7717 OO.ui.MenuLayout.prototype.updateSizes = function () {
7718         if ( this.showMenu ) {
7719                 this.$menu
7720                         .css( this.menuPosition.sizeProperty, this.menuSize )
7721                         .css( 'overflow', '' );
7722                 // Set offsets on all sides. CSS resets all but one with
7723                 // 'important' rules so directionality flips are supported
7724                 this.$content.css( {
7725                         top: this.menuSize,
7726                         right: this.menuSize,
7727                         bottom: this.menuSize,
7728                         left: this.menuSize
7729                 } );
7730         } else {
7731                 this.$menu
7732                         .css( this.menuPosition.sizeProperty, 0 )
7733                         .css( 'overflow', 'hidden' );
7734                 this.$content.css( {
7735                         top: 0,
7736                         right: 0,
7737                         bottom: 0,
7738                         left: 0
7739                 } );
7740         }
7744  * Get menu size.
7746  * @return {number|string} Menu size
7747  */
7748 OO.ui.MenuLayout.prototype.getMenuSize = function () {
7749         return this.menuSize;
7753  * Set menu position.
7755  * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
7756  * @throws {Error} If position value is not supported
7757  * @chainable
7758  */
7759 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
7760         var positions = this.constructor.static.menuPositions;
7762         if ( !positions[ position ] ) {
7763                 throw new Error( 'Cannot set position; unsupported position value: ' + position );
7764         }
7766         this.$menu.css( this.menuPosition.sizeProperty, '' );
7767         this.$element.removeClass( this.menuPosition.className );
7769         this.menuPosition = positions[ position ];
7771         this.updateSizes();
7772         this.$element.addClass( this.menuPosition.className );
7774         return this;
7778  * Get menu position.
7780  * @return {string} Menu position
7781  */
7782 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
7783         return this.menuPosition;
7787  * Layout containing a series of pages.
7789  * @class
7790  * @extends OO.ui.MenuLayout
7792  * @constructor
7793  * @param {Object} [config] Configuration options
7794  * @cfg {boolean} [continuous=false] Show all pages, one after another
7795  * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
7796  * @cfg {boolean} [outlined=false] Show an outline
7797  * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
7798  */
7799 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
7800         // Configuration initialization
7801         config = config || {};
7803         // Parent constructor
7804         OO.ui.BookletLayout.super.call( this, config );
7806         // Properties
7807         this.currentPageName = null;
7808         this.pages = {};
7809         this.ignoreFocus = false;
7810         this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
7811         this.$content.append( this.stackLayout.$element );
7812         this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
7813         this.outlineVisible = false;
7814         this.outlined = !!config.outlined;
7815         if ( this.outlined ) {
7816                 this.editable = !!config.editable;
7817                 this.outlineControlsWidget = null;
7818                 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
7819                 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
7820                 this.$menu.append( this.outlinePanel.$element );
7821                 this.outlineVisible = true;
7822                 if ( this.editable ) {
7823                         this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
7824                                 this.outlineSelectWidget
7825                         );
7826                 }
7827         }
7828         this.toggleMenu( this.outlined );
7830         // Events
7831         this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
7832         if ( this.outlined ) {
7833                 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
7834         }
7835         if ( this.autoFocus ) {
7836                 // Event 'focus' does not bubble, but 'focusin' does
7837                 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
7838         }
7840         // Initialization
7841         this.$element.addClass( 'oo-ui-bookletLayout' );
7842         this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
7843         if ( this.outlined ) {
7844                 this.outlinePanel.$element
7845                         .addClass( 'oo-ui-bookletLayout-outlinePanel' )
7846                         .append( this.outlineSelectWidget.$element );
7847                 if ( this.editable ) {
7848                         this.outlinePanel.$element
7849                                 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
7850                                 .append( this.outlineControlsWidget.$element );
7851                 }
7852         }
7855 /* Setup */
7857 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
7859 /* Events */
7862  * @event set
7863  * @param {OO.ui.PageLayout} page Current page
7864  */
7867  * @event add
7868  * @param {OO.ui.PageLayout[]} page Added pages
7869  * @param {number} index Index pages were added at
7870  */
7873  * @event remove
7874  * @param {OO.ui.PageLayout[]} pages Removed pages
7875  */
7877 /* Methods */
7880  * Handle stack layout focus.
7882  * @param {jQuery.Event} e Focusin event
7883  */
7884 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
7885         var name, $target;
7887         // Find the page that an element was focused within
7888         $target = $( e.target ).closest( '.oo-ui-pageLayout' );
7889         for ( name in this.pages ) {
7890                 // Check for page match, exclude current page to find only page changes
7891                 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
7892                         this.setPage( name );
7893                         break;
7894                 }
7895         }
7899  * Handle stack layout set events.
7901  * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
7902  */
7903 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
7904         var layout = this;
7905         if ( page ) {
7906                 page.scrollElementIntoView( { complete: function () {
7907                         if ( layout.autoFocus ) {
7908                                 layout.focus();
7909                         }
7910                 } } );
7911         }
7915  * Focus the first input in the current page.
7917  * If no page is selected, the first selectable page will be selected.
7918  * If the focus is already in an element on the current page, nothing will happen.
7919  */
7920 OO.ui.BookletLayout.prototype.focus = function () {
7921         var $input, page = this.stackLayout.getCurrentItem();
7922         if ( !page && this.outlined ) {
7923                 this.selectFirstSelectablePage();
7924                 page = this.stackLayout.getCurrentItem();
7925         }
7926         if ( !page ) {
7927                 return;
7928         }
7929         // Only change the focus if is not already in the current page
7930         if ( !page.$element.find( ':focus' ).length ) {
7931                 $input = page.$element.find( ':input:first' );
7932                 if ( $input.length ) {
7933                         $input[ 0 ].focus();
7934                 }
7935         }
7939  * Handle outline widget select events.
7941  * @param {OO.ui.OptionWidget|null} item Selected item
7942  */
7943 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
7944         if ( item ) {
7945                 this.setPage( item.getData() );
7946         }
7950  * Check if booklet has an outline.
7952  * @return {boolean}
7953  */
7954 OO.ui.BookletLayout.prototype.isOutlined = function () {
7955         return this.outlined;
7959  * Check if booklet has editing controls.
7961  * @return {boolean}
7962  */
7963 OO.ui.BookletLayout.prototype.isEditable = function () {
7964         return this.editable;
7968  * Check if booklet has a visible outline.
7970  * @return {boolean}
7971  */
7972 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
7973         return this.outlined && this.outlineVisible;
7977  * Hide or show the outline.
7979  * @param {boolean} [show] Show outline, omit to invert current state
7980  * @chainable
7981  */
7982 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
7983         if ( this.outlined ) {
7984                 show = show === undefined ? !this.outlineVisible : !!show;
7985                 this.outlineVisible = show;
7986                 this.toggleMenu( show );
7987         }
7989         return this;
7993  * Get the outline widget.
7995  * @param {OO.ui.PageLayout} page Page to be selected
7996  * @return {OO.ui.PageLayout|null} Closest page to another
7997  */
7998 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
7999         var next, prev, level,
8000                 pages = this.stackLayout.getItems(),
8001                 index = $.inArray( page, pages );
8003         if ( index !== -1 ) {
8004                 next = pages[ index + 1 ];
8005                 prev = pages[ index - 1 ];
8006                 // Prefer adjacent pages at the same level
8007                 if ( this.outlined ) {
8008                         level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
8009                         if (
8010                                 prev &&
8011                                 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
8012                         ) {
8013                                 return prev;
8014                         }
8015                         if (
8016                                 next &&
8017                                 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
8018                         ) {
8019                                 return next;
8020                         }
8021                 }
8022         }
8023         return prev || next || null;
8027  * Get the outline widget.
8029  * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
8030  */
8031 OO.ui.BookletLayout.prototype.getOutline = function () {
8032         return this.outlineSelectWidget;
8036  * Get the outline controls widget. If the outline is not editable, null is returned.
8038  * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
8039  */
8040 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
8041         return this.outlineControlsWidget;
8045  * Get a page by name.
8047  * @param {string} name Symbolic name of page
8048  * @return {OO.ui.PageLayout|undefined} Page, if found
8049  */
8050 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
8051         return this.pages[ name ];
8055  * Get the current page
8057  * @return {OO.ui.PageLayout|undefined} Current page, if found
8058  */
8059 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
8060         var name = this.getCurrentPageName();
8061         return name ? this.getPage( name ) : undefined;
8065  * Get the current page name.
8067  * @return {string|null} Current page name
8068  */
8069 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
8070         return this.currentPageName;
8074  * Add a page to the layout.
8076  * When pages are added with the same names as existing pages, the existing pages will be
8077  * automatically removed before the new pages are added.
8079  * @param {OO.ui.PageLayout[]} pages Pages to add
8080  * @param {number} index Index to insert pages after
8081  * @fires add
8082  * @chainable
8083  */
8084 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
8085         var i, len, name, page, item, currentIndex,
8086                 stackLayoutPages = this.stackLayout.getItems(),
8087                 remove = [],
8088                 items = [];
8090         // Remove pages with same names
8091         for ( i = 0, len = pages.length; i < len; i++ ) {
8092                 page = pages[ i ];
8093                 name = page.getName();
8095                 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
8096                         // Correct the insertion index
8097                         currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
8098                         if ( currentIndex !== -1 && currentIndex + 1 < index ) {
8099                                 index--;
8100                         }
8101                         remove.push( this.pages[ name ] );
8102                 }
8103         }
8104         if ( remove.length ) {
8105                 this.removePages( remove );
8106         }
8108         // Add new pages
8109         for ( i = 0, len = pages.length; i < len; i++ ) {
8110                 page = pages[ i ];
8111                 name = page.getName();
8112                 this.pages[ page.getName() ] = page;
8113                 if ( this.outlined ) {
8114                         item = new OO.ui.OutlineOptionWidget( { data: name } );
8115                         page.setOutlineItem( item );
8116                         items.push( item );
8117                 }
8118         }
8120         if ( this.outlined && items.length ) {
8121                 this.outlineSelectWidget.addItems( items, index );
8122                 this.selectFirstSelectablePage();
8123         }
8124         this.stackLayout.addItems( pages, index );
8125         this.emit( 'add', pages, index );
8127         return this;
8131  * Remove a page from the layout.
8133  * @fires remove
8134  * @chainable
8135  */
8136 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
8137         var i, len, name, page,
8138                 items = [];
8140         for ( i = 0, len = pages.length; i < len; i++ ) {
8141                 page = pages[ i ];
8142                 name = page.getName();
8143                 delete this.pages[ name ];
8144                 if ( this.outlined ) {
8145                         items.push( this.outlineSelectWidget.getItemFromData( name ) );
8146                         page.setOutlineItem( null );
8147                 }
8148         }
8149         if ( this.outlined && items.length ) {
8150                 this.outlineSelectWidget.removeItems( items );
8151                 this.selectFirstSelectablePage();
8152         }
8153         this.stackLayout.removeItems( pages );
8154         this.emit( 'remove', pages );
8156         return this;
8160  * Clear all pages from the layout.
8162  * @fires remove
8163  * @chainable
8164  */
8165 OO.ui.BookletLayout.prototype.clearPages = function () {
8166         var i, len,
8167                 pages = this.stackLayout.getItems();
8169         this.pages = {};
8170         this.currentPageName = null;
8171         if ( this.outlined ) {
8172                 this.outlineSelectWidget.clearItems();
8173                 for ( i = 0, len = pages.length; i < len; i++ ) {
8174                         pages[ i ].setOutlineItem( null );
8175                 }
8176         }
8177         this.stackLayout.clearItems();
8179         this.emit( 'remove', pages );
8181         return this;
8185  * Set the current page by name.
8187  * @fires set
8188  * @param {string} name Symbolic name of page
8189  */
8190 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
8191         var selectedItem,
8192                 $focused,
8193                 page = this.pages[ name ];
8195         if ( name !== this.currentPageName ) {
8196                 if ( this.outlined ) {
8197                         selectedItem = this.outlineSelectWidget.getSelectedItem();
8198                         if ( selectedItem && selectedItem.getData() !== name ) {
8199                                 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
8200                         }
8201                 }
8202                 if ( page ) {
8203                         if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
8204                                 this.pages[ this.currentPageName ].setActive( false );
8205                                 // Blur anything focused if the next page doesn't have anything focusable - this
8206                                 // is not needed if the next page has something focusable because once it is focused
8207                                 // this blur happens automatically
8208                                 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
8209                                         $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
8210                                         if ( $focused.length ) {
8211                                                 $focused[ 0 ].blur();
8212                                         }
8213                                 }
8214                         }
8215                         this.currentPageName = name;
8216                         this.stackLayout.setItem( page );
8217                         page.setActive( true );
8218                         this.emit( 'set', page );
8219                 }
8220         }
8224  * Select the first selectable page.
8226  * @chainable
8227  */
8228 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
8229         if ( !this.outlineSelectWidget.getSelectedItem() ) {
8230                 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
8231         }
8233         return this;
8237  * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
8239  * @class
8240  * @extends OO.ui.Layout
8242  * @constructor
8243  * @param {Object} [config] Configuration options
8244  * @cfg {boolean} [scrollable=false] Allow vertical scrolling
8245  * @cfg {boolean} [padded=false] Pad the content from the edges
8246  * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
8247  */
8248 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
8249         // Configuration initialization
8250         config = $.extend( {
8251                 scrollable: false,
8252                 padded: false,
8253                 expanded: true
8254         }, config );
8256         // Parent constructor
8257         OO.ui.PanelLayout.super.call( this, config );
8259         // Initialization
8260         this.$element.addClass( 'oo-ui-panelLayout' );
8261         if ( config.scrollable ) {
8262                 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
8263         }
8264         if ( config.padded ) {
8265                 this.$element.addClass( 'oo-ui-panelLayout-padded' );
8266         }
8267         if ( config.expanded ) {
8268                 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
8269         }
8272 /* Setup */
8274 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
8277  * Page within an booklet layout.
8279  * @class
8280  * @extends OO.ui.PanelLayout
8282  * @constructor
8283  * @param {string} name Unique symbolic name of page
8284  * @param {Object} [config] Configuration options
8285  */
8286 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
8287         // Configuration initialization
8288         config = $.extend( { scrollable: true }, config );
8290         // Parent constructor
8291         OO.ui.PageLayout.super.call( this, config );
8293         // Properties
8294         this.name = name;
8295         this.outlineItem = null;
8296         this.active = false;
8298         // Initialization
8299         this.$element.addClass( 'oo-ui-pageLayout' );
8302 /* Setup */
8304 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
8306 /* Events */
8309  * @event active
8310  * @param {boolean} active Page is active
8311  */
8313 /* Methods */
8316  * Get page name.
8318  * @return {string} Symbolic name of page
8319  */
8320 OO.ui.PageLayout.prototype.getName = function () {
8321         return this.name;
8325  * Check if page is active.
8327  * @return {boolean} Page is active
8328  */
8329 OO.ui.PageLayout.prototype.isActive = function () {
8330         return this.active;
8334  * Get outline item.
8336  * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
8337  */
8338 OO.ui.PageLayout.prototype.getOutlineItem = function () {
8339         return this.outlineItem;
8343  * Set outline item.
8345  * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
8346  *   outline item as desired; this method is called for setting (with an object) and unsetting
8347  *   (with null) and overriding methods would have to check the value of `outlineItem` to avoid
8348  *   operating on null instead of an OO.ui.OutlineOptionWidget object.
8350  * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
8351  * @chainable
8352  */
8353 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
8354         this.outlineItem = outlineItem || null;
8355         if ( outlineItem ) {
8356                 this.setupOutlineItem();
8357         }
8358         return this;
8362  * Setup outline item.
8364  * @localdoc Subclasses should override this method to adjust the outline item as desired.
8366  * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
8367  * @chainable
8368  */
8369 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
8370         return this;
8374  * Set page active state.
8376  * @param {boolean} Page is active
8377  * @fires active
8378  */
8379 OO.ui.PageLayout.prototype.setActive = function ( active ) {
8380         active = !!active;
8382         if ( active !== this.active ) {
8383                 this.active = active;
8384                 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
8385                 this.emit( 'active', this.active );
8386         }
8390  * Layout containing a series of mutually exclusive pages.
8392  * @class
8393  * @extends OO.ui.PanelLayout
8394  * @mixins OO.ui.GroupElement
8396  * @constructor
8397  * @param {Object} [config] Configuration options
8398  * @cfg {boolean} [continuous=false] Show all pages, one after another
8399  * @cfg {OO.ui.Layout[]} [items] Layouts to add
8400  */
8401 OO.ui.StackLayout = function OoUiStackLayout( config ) {
8402         // Configuration initialization
8403         config = $.extend( { scrollable: true }, config );
8405         // Parent constructor
8406         OO.ui.StackLayout.super.call( this, config );
8408         // Mixin constructors
8409         OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8411         // Properties
8412         this.currentItem = null;
8413         this.continuous = !!config.continuous;
8415         // Initialization
8416         this.$element.addClass( 'oo-ui-stackLayout' );
8417         if ( this.continuous ) {
8418                 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
8419         }
8420         if ( Array.isArray( config.items ) ) {
8421                 this.addItems( config.items );
8422         }
8425 /* Setup */
8427 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
8428 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
8430 /* Events */
8433  * @event set
8434  * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
8435  */
8437 /* Methods */
8440  * Get the current item.
8442  * @return {OO.ui.Layout|null}
8443  */
8444 OO.ui.StackLayout.prototype.getCurrentItem = function () {
8445         return this.currentItem;
8449  * Unset the current item.
8451  * @private
8452  * @param {OO.ui.StackLayout} layout
8453  * @fires set
8454  */
8455 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
8456         var prevItem = this.currentItem;
8457         if ( prevItem === null ) {
8458                 return;
8459         }
8461         this.currentItem = null;
8462         this.emit( 'set', null );
8466  * Add items.
8468  * Adding an existing item (by value) will move it.
8470  * @param {OO.ui.Layout[]} items Items to add
8471  * @param {number} [index] Index to insert items after
8472  * @chainable
8473  */
8474 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
8475         // Update the visibility
8476         this.updateHiddenState( items, this.currentItem );
8478         // Mixin method
8479         OO.ui.GroupElement.prototype.addItems.call( this, items, index );
8481         if ( !this.currentItem && items.length ) {
8482                 this.setItem( items[ 0 ] );
8483         }
8485         return this;
8489  * Remove items.
8491  * Items will be detached, not removed, so they can be used later.
8493  * @param {OO.ui.Layout[]} items Items to remove
8494  * @chainable
8495  * @fires set
8496  */
8497 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
8498         // Mixin method
8499         OO.ui.GroupElement.prototype.removeItems.call( this, items );
8501         if ( $.inArray( this.currentItem, items ) !== -1 ) {
8502                 if ( this.items.length ) {
8503                         this.setItem( this.items[ 0 ] );
8504                 } else {
8505                         this.unsetCurrentItem();
8506                 }
8507         }
8509         return this;
8513  * Clear all items.
8515  * Items will be detached, not removed, so they can be used later.
8517  * @chainable
8518  * @fires set
8519  */
8520 OO.ui.StackLayout.prototype.clearItems = function () {
8521         this.unsetCurrentItem();
8522         OO.ui.GroupElement.prototype.clearItems.call( this );
8524         return this;
8528  * Show item.
8530  * Any currently shown item will be hidden.
8532  * FIXME: If the passed item to show has not been added in the items list, then
8533  * this method drops it and unsets the current item.
8535  * @param {OO.ui.Layout} item Item to show
8536  * @chainable
8537  * @fires set
8538  */
8539 OO.ui.StackLayout.prototype.setItem = function ( item ) {
8540         if ( item !== this.currentItem ) {
8541                 this.updateHiddenState( this.items, item );
8543                 if ( $.inArray( item, this.items ) !== -1 ) {
8544                         this.currentItem = item;
8545                         this.emit( 'set', item );
8546                 } else {
8547                         this.unsetCurrentItem();
8548                 }
8549         }
8551         return this;
8555  * Update the visibility of all items in case of non-continuous view.
8557  * Ensure all items are hidden except for the selected one.
8558  * This method does nothing when the stack is continuous.
8560  * @param {OO.ui.Layout[]} items Item list iterate over
8561  * @param {OO.ui.Layout} [selectedItem] Selected item to show
8562  */
8563 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
8564         var i, len;
8566         if ( !this.continuous ) {
8567                 for ( i = 0, len = items.length; i < len; i++ ) {
8568                         if ( !selectedItem || selectedItem !== items[ i ] ) {
8569                                 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
8570                         }
8571                 }
8572                 if ( selectedItem ) {
8573                         selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
8574                 }
8575         }
8579  * Horizontal bar layout of tools as icon buttons.
8581  * @class
8582  * @extends OO.ui.ToolGroup
8584  * @constructor
8585  * @param {OO.ui.Toolbar} toolbar
8586  * @param {Object} [config] Configuration options
8587  */
8588 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
8589         // Parent constructor
8590         OO.ui.BarToolGroup.super.call( this, toolbar, config );
8592         // Initialization
8593         this.$element.addClass( 'oo-ui-barToolGroup' );
8596 /* Setup */
8598 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
8600 /* Static Properties */
8602 OO.ui.BarToolGroup.static.titleTooltips = true;
8604 OO.ui.BarToolGroup.static.accelTooltips = true;
8606 OO.ui.BarToolGroup.static.name = 'bar';
8609  * Popup list of tools with an icon and optional label.
8611  * @abstract
8612  * @class
8613  * @extends OO.ui.ToolGroup
8614  * @mixins OO.ui.IconElement
8615  * @mixins OO.ui.IndicatorElement
8616  * @mixins OO.ui.LabelElement
8617  * @mixins OO.ui.TitledElement
8618  * @mixins OO.ui.ClippableElement
8620  * @constructor
8621  * @param {OO.ui.Toolbar} toolbar
8622  * @param {Object} [config] Configuration options
8623  * @cfg {string} [header] Text to display at the top of the pop-up
8624  */
8625 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
8626         // Configuration initialization
8627         config = config || {};
8629         // Parent constructor
8630         OO.ui.PopupToolGroup.super.call( this, toolbar, config );
8632         // Mixin constructors
8633         OO.ui.IconElement.call( this, config );
8634         OO.ui.IndicatorElement.call( this, config );
8635         OO.ui.LabelElement.call( this, config );
8636         OO.ui.TitledElement.call( this, config );
8637         OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
8639         // Properties
8640         this.active = false;
8641         this.dragging = false;
8642         this.onBlurHandler = this.onBlur.bind( this );
8643         this.$handle = $( '<span>' );
8645         // Events
8646         this.$handle.on( {
8647                 'mousedown touchstart': this.onHandlePointerDown.bind( this ),
8648                 'mouseup touchend': this.onHandlePointerUp.bind( this )
8649         } );
8651         // Initialization
8652         this.$handle
8653                 .addClass( 'oo-ui-popupToolGroup-handle' )
8654                 .append( this.$icon, this.$label, this.$indicator );
8655         // If the pop-up should have a header, add it to the top of the toolGroup.
8656         // Note: If this feature is useful for other widgets, we could abstract it into an
8657         // OO.ui.HeaderedElement mixin constructor.
8658         if ( config.header !== undefined ) {
8659                 this.$group
8660                         .prepend( $( '<span>' )
8661                                 .addClass( 'oo-ui-popupToolGroup-header' )
8662                                 .text( config.header )
8663                         );
8664         }
8665         this.$element
8666                 .addClass( 'oo-ui-popupToolGroup' )
8667                 .prepend( this.$handle );
8670 /* Setup */
8672 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
8673 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement );
8674 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement );
8675 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement );
8676 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
8677 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
8679 /* Static Properties */
8681 /* Methods */
8684  * @inheritdoc
8685  */
8686 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
8687         // Parent method
8688         OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
8690         if ( this.isDisabled() && this.isElementAttached() ) {
8691                 this.setActive( false );
8692         }
8696  * Handle focus being lost.
8698  * The event is actually generated from a mouseup, so it is not a normal blur event object.
8700  * @param {jQuery.Event} e Mouse up event
8701  */
8702 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
8703         // Only deactivate when clicking outside the dropdown element
8704         if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
8705                 this.setActive( false );
8706         }
8710  * @inheritdoc
8711  */
8712 OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) {
8713         // e.which is 0 for touch events, 1 for left mouse button
8714         // Only close toolgroup when a tool was actually selected
8715         // FIXME: this duplicates logic from the parent class
8716         if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === this.getTargetTool( e ) ) {
8717                 this.setActive( false );
8718         }
8719         return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e );
8723  * Handle mouse up events.
8725  * @param {jQuery.Event} e Mouse up event
8726  */
8727 OO.ui.PopupToolGroup.prototype.onHandlePointerUp = function () {
8728         return false;
8732  * Handle mouse down events.
8734  * @param {jQuery.Event} e Mouse down event
8735  */
8736 OO.ui.PopupToolGroup.prototype.onHandlePointerDown = function ( e ) {
8737         // e.which is 0 for touch events, 1 for left mouse button
8738         if ( !this.isDisabled() && e.which <= 1 ) {
8739                 this.setActive( !this.active );
8740         }
8741         return false;
8745  * Switch into active mode.
8747  * When active, mouseup events anywhere in the document will trigger deactivation.
8748  */
8749 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
8750         value = !!value;
8751         if ( this.active !== value ) {
8752                 this.active = value;
8753                 if ( value ) {
8754                         this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
8756                         // Try anchoring the popup to the left first
8757                         this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
8758                         this.toggleClipping( true );
8759                         if ( this.isClippedHorizontally() ) {
8760                                 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
8761                                 this.toggleClipping( false );
8762                                 this.$element
8763                                         .removeClass( 'oo-ui-popupToolGroup-left' )
8764                                         .addClass( 'oo-ui-popupToolGroup-right' );
8765                                 this.toggleClipping( true );
8766                         }
8767                 } else {
8768                         this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
8769                         this.$element.removeClass(
8770                                 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left  oo-ui-popupToolGroup-right'
8771                         );
8772                         this.toggleClipping( false );
8773                 }
8774         }
8778  * Drop down list layout of tools as labeled icon buttons.
8780  * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
8781  * bottom of the main list. These are not automatically positioned at the bottom of the list; you
8782  * may want to use the 'promote' and 'demote' configuration options to achieve this.
8784  * @class
8785  * @extends OO.ui.PopupToolGroup
8787  * @constructor
8788  * @param {OO.ui.Toolbar} toolbar
8789  * @param {Object} [config] Configuration options
8790  * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
8791  *  shown.
8792  * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
8793  *  allowed to be collapsed.
8794  * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
8795  */
8796 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
8797         // Configuration initialization
8798         config = config || {};
8800         // Properties (must be set before parent constructor, which calls #populate)
8801         this.allowCollapse = config.allowCollapse;
8802         this.forceExpand = config.forceExpand;
8803         this.expanded = config.expanded !== undefined ? config.expanded : false;
8804         this.collapsibleTools = [];
8806         // Parent constructor
8807         OO.ui.ListToolGroup.super.call( this, toolbar, config );
8809         // Initialization
8810         this.$element.addClass( 'oo-ui-listToolGroup' );
8813 /* Setup */
8815 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
8817 /* Static Properties */
8819 OO.ui.ListToolGroup.static.accelTooltips = true;
8821 OO.ui.ListToolGroup.static.name = 'list';
8823 /* Methods */
8826  * @inheritdoc
8827  */
8828 OO.ui.ListToolGroup.prototype.populate = function () {
8829         var i, len, allowCollapse = [];
8831         OO.ui.ListToolGroup.super.prototype.populate.call( this );
8833         // Update the list of collapsible tools
8834         if ( this.allowCollapse !== undefined ) {
8835                 allowCollapse = this.allowCollapse;
8836         } else if ( this.forceExpand !== undefined ) {
8837                 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
8838         }
8840         this.collapsibleTools = [];
8841         for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
8842                 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
8843                         this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
8844                 }
8845         }
8847         // Keep at the end, even when tools are added
8848         this.$group.append( this.getExpandCollapseTool().$element );
8850         this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
8851         this.updateCollapsibleState();
8854 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
8855         if ( this.expandCollapseTool === undefined ) {
8856                 var ExpandCollapseTool = function () {
8857                         ExpandCollapseTool.super.apply( this, arguments );
8858                 };
8860                 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
8862                 ExpandCollapseTool.prototype.onSelect = function () {
8863                         this.toolGroup.expanded = !this.toolGroup.expanded;
8864                         this.toolGroup.updateCollapsibleState();
8865                         this.setActive( false );
8866                 };
8867                 ExpandCollapseTool.prototype.onUpdateState = function () {
8868                         // Do nothing. Tool interface requires an implementation of this function.
8869                 };
8871                 ExpandCollapseTool.static.name = 'more-fewer';
8873                 this.expandCollapseTool = new ExpandCollapseTool( this );
8874         }
8875         return this.expandCollapseTool;
8879  * @inheritdoc
8880  */
8881 OO.ui.ListToolGroup.prototype.onPointerUp = function ( e ) {
8882         var ret = OO.ui.ListToolGroup.super.prototype.onPointerUp.call( this, e );
8884         // Do not close the popup when the user wants to show more/fewer tools
8885         if ( $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
8886                 // Prevent the popup list from being hidden
8887                 this.setActive( true );
8888         }
8890         return ret;
8893 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
8894         var i, len;
8896         this.getExpandCollapseTool()
8897                 .setIcon( this.expanded ? 'collapse' : 'expand' )
8898                 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
8900         for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
8901                 this.collapsibleTools[ i ].toggle( this.expanded );
8902         }
8906  * Drop down menu layout of tools as selectable menu items.
8908  * @class
8909  * @extends OO.ui.PopupToolGroup
8911  * @constructor
8912  * @param {OO.ui.Toolbar} toolbar
8913  * @param {Object} [config] Configuration options
8914  */
8915 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
8916         // Configuration initialization
8917         config = config || {};
8919         // Parent constructor
8920         OO.ui.MenuToolGroup.super.call( this, toolbar, config );
8922         // Events
8923         this.toolbar.connect( this, { updateState: 'onUpdateState' } );
8925         // Initialization
8926         this.$element.addClass( 'oo-ui-menuToolGroup' );
8929 /* Setup */
8931 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
8933 /* Static Properties */
8935 OO.ui.MenuToolGroup.static.accelTooltips = true;
8937 OO.ui.MenuToolGroup.static.name = 'menu';
8939 /* Methods */
8942  * Handle the toolbar state being updated.
8944  * When the state changes, the title of each active item in the menu will be joined together and
8945  * used as a label for the group. The label will be empty if none of the items are active.
8946  */
8947 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
8948         var name,
8949                 labelTexts = [];
8951         for ( name in this.tools ) {
8952                 if ( this.tools[ name ].isActive() ) {
8953                         labelTexts.push( this.tools[ name ].getTitle() );
8954                 }
8955         }
8957         this.setLabel( labelTexts.join( ', ' ) || ' ' );
8961  * Tool that shows a popup when selected.
8963  * @abstract
8964  * @class
8965  * @extends OO.ui.Tool
8966  * @mixins OO.ui.PopupElement
8968  * @constructor
8969  * @param {OO.ui.Toolbar} toolbar
8970  * @param {Object} [config] Configuration options
8971  */
8972 OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
8973         // Parent constructor
8974         OO.ui.PopupTool.super.call( this, toolbar, config );
8976         // Mixin constructors
8977         OO.ui.PopupElement.call( this, config );
8979         // Initialization
8980         this.$element
8981                 .addClass( 'oo-ui-popupTool' )
8982                 .append( this.popup.$element );
8985 /* Setup */
8987 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
8988 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement );
8990 /* Methods */
8993  * Handle the tool being selected.
8995  * @inheritdoc
8996  */
8997 OO.ui.PopupTool.prototype.onSelect = function () {
8998         if ( !this.isDisabled() ) {
8999                 this.popup.toggle();
9000         }
9001         this.setActive( false );
9002         return false;
9006  * Handle the toolbar state being updated.
9008  * @inheritdoc
9009  */
9010 OO.ui.PopupTool.prototype.onUpdateState = function () {
9011         this.setActive( false );
9015  * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
9017  * Use together with OO.ui.ItemWidget to make disabled state inheritable.
9019  * @abstract
9020  * @class
9021  * @extends OO.ui.GroupElement
9023  * @constructor
9024  * @param {Object} [config] Configuration options
9025  */
9026 OO.ui.GroupWidget = function OoUiGroupWidget( config ) {
9027         // Parent constructor
9028         OO.ui.GroupWidget.super.call( this, config );
9031 /* Setup */
9033 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
9035 /* Methods */
9038  * Set the disabled state of the widget.
9040  * This will also update the disabled state of child widgets.
9042  * @param {boolean} disabled Disable widget
9043  * @chainable
9044  */
9045 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
9046         var i, len;
9048         // Parent method
9049         // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
9050         OO.ui.Widget.prototype.setDisabled.call( this, disabled );
9052         // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
9053         if ( this.items ) {
9054                 for ( i = 0, len = this.items.length; i < len; i++ ) {
9055                         this.items[ i ].updateDisabled();
9056                 }
9057         }
9059         return this;
9063  * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
9065  * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
9066  * allows bidirectional communication.
9068  * Use together with OO.ui.GroupWidget to make disabled state inheritable.
9070  * @abstract
9071  * @class
9073  * @constructor
9074  */
9075 OO.ui.ItemWidget = function OoUiItemWidget() {
9076         //
9079 /* Methods */
9082  * Check if widget is disabled.
9084  * Checks parent if present, making disabled state inheritable.
9086  * @return {boolean} Widget is disabled
9087  */
9088 OO.ui.ItemWidget.prototype.isDisabled = function () {
9089         return this.disabled ||
9090                 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
9094  * Set group element is in.
9096  * @param {OO.ui.GroupElement|null} group Group element, null if none
9097  * @chainable
9098  */
9099 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
9100         // Parent method
9101         // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
9102         OO.ui.Element.prototype.setElementGroup.call( this, group );
9104         // Initialize item disabled states
9105         this.updateDisabled();
9107         return this;
9111  * Mixin that adds a menu showing suggested values for a text input.
9113  * Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections.
9115  * Subclasses that set the value of #lookupInput from their `choose` or `select` handler should
9116  * be aware that this will cause new suggestions to be looked up for the new value. If this is
9117  * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
9119  * @class
9120  * @abstract
9121  * @deprecated Use OO.ui.LookupElement instead.
9123  * @constructor
9124  * @param {OO.ui.TextInputWidget} input Input widget
9125  * @param {Object} [config] Configuration options
9126  * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
9127  * @cfg {jQuery} [$container=input.$element] Element to render menu under
9128  */
9129 OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
9130         // Configuration initialization
9131         config = config || {};
9133         // Properties
9134         this.lookupInput = input;
9135         this.$overlay = config.$overlay || this.$element;
9136         this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
9137                 input: this.lookupInput,
9138                 $container: config.$container
9139         } );
9140         this.lookupCache = {};
9141         this.lookupQuery = null;
9142         this.lookupRequest = null;
9143         this.lookupsDisabled = false;
9144         this.lookupInputFocused = false;
9146         // Events
9147         this.lookupInput.$input.on( {
9148                 focus: this.onLookupInputFocus.bind( this ),
9149                 blur: this.onLookupInputBlur.bind( this ),
9150                 mousedown: this.onLookupInputMouseDown.bind( this )
9151         } );
9152         this.lookupInput.connect( this, { change: 'onLookupInputChange' } );
9153         this.lookupMenu.connect( this, { toggle: 'onLookupMenuToggle' } );
9155         // Initialization
9156         this.$element.addClass( 'oo-ui-lookupWidget' );
9157         this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
9158         this.$overlay.append( this.lookupMenu.$element );
9161 /* Methods */
9164  * Handle input focus event.
9166  * @param {jQuery.Event} e Input focus event
9167  */
9168 OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
9169         this.lookupInputFocused = true;
9170         this.populateLookupMenu();
9174  * Handle input blur event.
9176  * @param {jQuery.Event} e Input blur event
9177  */
9178 OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
9179         this.closeLookupMenu();
9180         this.lookupInputFocused = false;
9184  * Handle input mouse down event.
9186  * @param {jQuery.Event} e Input mouse down event
9187  */
9188 OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
9189         // Only open the menu if the input was already focused.
9190         // This way we allow the user to open the menu again after closing it with Esc
9191         // by clicking in the input. Opening (and populating) the menu when initially
9192         // clicking into the input is handled by the focus handler.
9193         if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
9194                 this.populateLookupMenu();
9195         }
9199  * Handle input change event.
9201  * @param {string} value New input value
9202  */
9203 OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
9204         if ( this.lookupInputFocused ) {
9205                 this.populateLookupMenu();
9206         }
9210  * Handle the lookup menu being shown/hidden.
9211  * @param {boolean} visible Whether the lookup menu is now visible.
9212  */
9213 OO.ui.LookupInputWidget.prototype.onLookupMenuToggle = function ( visible ) {
9214         if ( !visible ) {
9215                 // When the menu is hidden, abort any active request and clear the menu.
9216                 // This has to be done here in addition to closeLookupMenu(), because
9217                 // MenuSelectWidget will close itself when the user presses Esc.
9218                 this.abortLookupRequest();
9219                 this.lookupMenu.clearItems();
9220         }
9224  * Get lookup menu.
9226  * @return {OO.ui.TextInputMenuSelectWidget}
9227  */
9228 OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
9229         return this.lookupMenu;
9233  * Disable or re-enable lookups.
9235  * When lookups are disabled, calls to #populateLookupMenu will be ignored.
9237  * @param {boolean} disabled Disable lookups
9238  */
9239 OO.ui.LookupInputWidget.prototype.setLookupsDisabled = function ( disabled ) {
9240         this.lookupsDisabled = !!disabled;
9244  * Open the menu. If there are no entries in the menu, this does nothing.
9246  * @chainable
9247  */
9248 OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
9249         if ( !this.lookupMenu.isEmpty() ) {
9250                 this.lookupMenu.toggle( true );
9251         }
9252         return this;
9256  * Close the menu, empty it, and abort any pending request.
9258  * @chainable
9259  */
9260 OO.ui.LookupInputWidget.prototype.closeLookupMenu = function () {
9261         this.lookupMenu.toggle( false );
9262         this.abortLookupRequest();
9263         this.lookupMenu.clearItems();
9264         return this;
9268  * Request menu items based on the input's current value, and when they arrive,
9269  * populate the menu with these items and show the menu.
9271  * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
9273  * @chainable
9274  */
9275 OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
9276         var widget = this,
9277                 value = this.lookupInput.getValue();
9279         if ( this.lookupsDisabled ) {
9280                 return;
9281         }
9283         // If the input is empty, clear the menu
9284         if ( value === '' ) {
9285                 this.closeLookupMenu();
9286         // Skip population if there is already a request pending for the current value
9287         } else if ( value !== this.lookupQuery ) {
9288                 this.getLookupMenuItems()
9289                         .done( function ( items ) {
9290                                 widget.lookupMenu.clearItems();
9291                                 if ( items.length ) {
9292                                         widget.lookupMenu
9293                                                 .addItems( items )
9294                                                 .toggle( true );
9295                                         widget.initializeLookupMenuSelection();
9296                                 } else {
9297                                         widget.lookupMenu.toggle( false );
9298                                 }
9299                         } )
9300                         .fail( function () {
9301                                 widget.lookupMenu.clearItems();
9302                         } );
9303         }
9305         return this;
9309  * Select and highlight the first selectable item in the menu.
9311  * @chainable
9312  */
9313 OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
9314         if ( !this.lookupMenu.getSelectedItem() ) {
9315                 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
9316         }
9317         this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
9321  * Get lookup menu items for the current query.
9323  * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
9324  * of the done event. If the request was aborted to make way for a subsequent request,
9325  * this promise will not be rejected: it will remain pending forever.
9326  */
9327 OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
9328         var widget = this,
9329                 value = this.lookupInput.getValue(),
9330                 deferred = $.Deferred(),
9331                 ourRequest;
9333         this.abortLookupRequest();
9334         if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
9335                 deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[ value ] ) );
9336         } else {
9337                 this.lookupInput.pushPending();
9338                 this.lookupQuery = value;
9339                 ourRequest = this.lookupRequest = this.getLookupRequest();
9340                 ourRequest
9341                         .always( function () {
9342                                 // We need to pop pending even if this is an old request, otherwise
9343                                 // the widget will remain pending forever.
9344                                 // TODO: this assumes that an aborted request will fail or succeed soon after
9345                                 // being aborted, or at least eventually. It would be nice if we could popPending()
9346                                 // at abort time, but only if we knew that we hadn't already called popPending()
9347                                 // for that request.
9348                                 widget.lookupInput.popPending();
9349                         } )
9350                         .done( function ( data ) {
9351                                 // If this is an old request (and aborting it somehow caused it to still succeed),
9352                                 // ignore its success completely
9353                                 if ( ourRequest === widget.lookupRequest ) {
9354                                         widget.lookupQuery = null;
9355                                         widget.lookupRequest = null;
9356                                         widget.lookupCache[ value ] = widget.getLookupCacheItemFromData( data );
9357                                         deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[ value ] ) );
9358                                 }
9359                         } )
9360                         .fail( function () {
9361                                 // If this is an old request (or a request failing because it's being aborted),
9362                                 // ignore its failure completely
9363                                 if ( ourRequest === widget.lookupRequest ) {
9364                                         widget.lookupQuery = null;
9365                                         widget.lookupRequest = null;
9366                                         deferred.reject();
9367                                 }
9368                         } );
9369         }
9370         return deferred.promise();
9374  * Abort the currently pending lookup request, if any.
9375  */
9376 OO.ui.LookupInputWidget.prototype.abortLookupRequest = function () {
9377         var oldRequest = this.lookupRequest;
9378         if ( oldRequest ) {
9379                 // First unset this.lookupRequest to the fail handler will notice
9380                 // that the request is no longer current
9381                 this.lookupRequest = null;
9382                 this.lookupQuery = null;
9383                 oldRequest.abort();
9384         }
9388  * Get a new request object of the current lookup query value.
9390  * @abstract
9391  * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
9392  */
9393 OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
9394         // Stub, implemented in subclass
9395         return null;
9399  * Get a list of menu item widgets from the data stored by the lookup request's done handler.
9401  * @abstract
9402  * @param {Mixed} data Cached result data, usually an array
9403  * @return {OO.ui.MenuOptionWidget[]} Menu items
9404  */
9405 OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
9406         // Stub, implemented in subclass
9407         return [];
9411  * Get lookup cache item from server response data.
9413  * @abstract
9414  * @param {Mixed} data Response from server
9415  * @return {Mixed} Cached result data
9416  */
9417 OO.ui.LookupInputWidget.prototype.getLookupCacheItemFromData = function () {
9418         // Stub, implemented in subclass
9419         return [];
9423  * Set of controls for an OO.ui.OutlineSelectWidget.
9425  * Controls include moving items up and down, removing items, and adding different kinds of items.
9427  * @class
9428  * @extends OO.ui.Widget
9429  * @mixins OO.ui.GroupElement
9430  * @mixins OO.ui.IconElement
9432  * @constructor
9433  * @param {OO.ui.OutlineSelectWidget} outline Outline to control
9434  * @param {Object} [config] Configuration options
9435  */
9436 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
9437         // Configuration initialization
9438         config = $.extend( { icon: 'add' }, config );
9440         // Parent constructor
9441         OO.ui.OutlineControlsWidget.super.call( this, config );
9443         // Mixin constructors
9444         OO.ui.GroupElement.call( this, config );
9445         OO.ui.IconElement.call( this, config );
9447         // Properties
9448         this.outline = outline;
9449         this.$movers = $( '<div>' );
9450         this.upButton = new OO.ui.ButtonWidget( {
9451                 framed: false,
9452                 icon: 'collapse',
9453                 title: OO.ui.msg( 'ooui-outline-control-move-up' )
9454         } );
9455         this.downButton = new OO.ui.ButtonWidget( {
9456                 framed: false,
9457                 icon: 'expand',
9458                 title: OO.ui.msg( 'ooui-outline-control-move-down' )
9459         } );
9460         this.removeButton = new OO.ui.ButtonWidget( {
9461                 framed: false,
9462                 icon: 'remove',
9463                 title: OO.ui.msg( 'ooui-outline-control-remove' )
9464         } );
9466         // Events
9467         outline.connect( this, {
9468                 select: 'onOutlineChange',
9469                 add: 'onOutlineChange',
9470                 remove: 'onOutlineChange'
9471         } );
9472         this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
9473         this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
9474         this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
9476         // Initialization
9477         this.$element.addClass( 'oo-ui-outlineControlsWidget' );
9478         this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
9479         this.$movers
9480                 .addClass( 'oo-ui-outlineControlsWidget-movers' )
9481                 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
9482         this.$element.append( this.$icon, this.$group, this.$movers );
9485 /* Setup */
9487 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
9488 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
9489 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement );
9491 /* Events */
9494  * @event move
9495  * @param {number} places Number of places to move
9496  */
9499  * @event remove
9500  */
9502 /* Methods */
9505  * Handle outline change events.
9506  */
9507 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
9508         var i, len, firstMovable, lastMovable,
9509                 items = this.outline.getItems(),
9510                 selectedItem = this.outline.getSelectedItem(),
9511                 movable = selectedItem && selectedItem.isMovable(),
9512                 removable = selectedItem && selectedItem.isRemovable();
9514         if ( movable ) {
9515                 i = -1;
9516                 len = items.length;
9517                 while ( ++i < len ) {
9518                         if ( items[ i ].isMovable() ) {
9519                                 firstMovable = items[ i ];
9520                                 break;
9521                         }
9522                 }
9523                 i = len;
9524                 while ( i-- ) {
9525                         if ( items[ i ].isMovable() ) {
9526                                 lastMovable = items[ i ];
9527                                 break;
9528                         }
9529                 }
9530         }
9531         this.upButton.setDisabled( !movable || selectedItem === firstMovable );
9532         this.downButton.setDisabled( !movable || selectedItem === lastMovable );
9533         this.removeButton.setDisabled( !removable );
9537  * Mixin for widgets with a boolean on/off state.
9539  * @abstract
9540  * @class
9542  * @constructor
9543  * @param {Object} [config] Configuration options
9544  * @cfg {boolean} [value=false] Initial value
9545  */
9546 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
9547         // Configuration initialization
9548         config = config || {};
9550         // Properties
9551         this.value = null;
9553         // Initialization
9554         this.$element.addClass( 'oo-ui-toggleWidget' );
9555         this.setValue( !!config.value );
9558 /* Events */
9561  * @event change
9562  * @param {boolean} value Changed value
9563  */
9565 /* Methods */
9568  * Get the value of the toggle.
9570  * @return {boolean}
9571  */
9572 OO.ui.ToggleWidget.prototype.getValue = function () {
9573         return this.value;
9577  * Set the value of the toggle.
9579  * @param {boolean} value New value
9580  * @fires change
9581  * @chainable
9582  */
9583 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
9584         value = !!value;
9585         if ( this.value !== value ) {
9586                 this.value = value;
9587                 this.emit( 'change', value );
9588                 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
9589                 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
9590                 this.$element.attr( 'aria-checked', value.toString() );
9591         }
9592         return this;
9596  * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
9597  * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
9598  * removed, and cleared from the group.
9600  *     @example
9601  *     // Example: A ButtonGroupWidget with two buttons
9602  *     var button1 = new OO.ui.PopupButtonWidget( {
9603  *         label : 'Select a category',
9604  *         icon : 'menu',
9605  *         popup : {
9606  *             $content: $( '<p>List of categories...</p>' ),
9607  *             padded: true,
9608  *             align: 'left'
9609  *         }
9610  *     } );
9611  *     var button2 = new OO.ui.ButtonWidget( {
9612  *         label : 'Add item'
9613  *     });
9614  *     var buttonGroup = new OO.ui.ButtonGroupWidget( {
9615  *         items: [button1, button2]
9616  *     } );
9617  *     $('body').append(buttonGroup.$element);
9619  * @class
9620  * @extends OO.ui.Widget
9621  * @mixins OO.ui.GroupElement
9623  * @constructor
9624  * @param {Object} [config] Configuration options
9625  * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
9626  */
9627 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
9628         // Configuration initialization
9629         config = config || {};
9631         // Parent constructor
9632         OO.ui.ButtonGroupWidget.super.call( this, config );
9634         // Mixin constructors
9635         OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9637         // Initialization
9638         this.$element.addClass( 'oo-ui-buttonGroupWidget' );
9639         if ( Array.isArray( config.items ) ) {
9640                 this.addItems( config.items );
9641         }
9644 /* Setup */
9646 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
9647 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
9650  * ButtonWidget is a generic widget for buttons. A wide variety of looks,
9651  * feels, and functionality can be customized via the class’s configuration options
9652  * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9653  * and examples.
9655  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
9657  *     @example
9658  *     // A button widget
9659  *     var button = new OO.ui.ButtonWidget( {
9660  *         label : 'Button with Icon',
9661  *         icon : 'remove',
9662  *         iconTitle : 'Remove'
9663  *     } );
9664  *     $( 'body' ).append( button.$element );
9666  * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
9668  * @class
9669  * @extends OO.ui.Widget
9670  * @mixins OO.ui.ButtonElement
9671  * @mixins OO.ui.IconElement
9672  * @mixins OO.ui.IndicatorElement
9673  * @mixins OO.ui.LabelElement
9674  * @mixins OO.ui.TitledElement
9675  * @mixins OO.ui.FlaggedElement
9676  * @mixins OO.ui.TabIndexedElement
9678  * @constructor
9679  * @param {Object} [config] Configuration options
9680  * @cfg {string} [href] Hyperlink to visit when the button is clicked.
9681  * @cfg {string} [target] The frame or window in which to open the hyperlink.
9682  * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
9683  */
9684 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
9685         // Configuration initialization
9686         // FIXME: The `nofollow` alias is deprecated and will be removed (T89767)
9687         config = $.extend( { noFollow: config && config.nofollow }, config );
9689         // Parent constructor
9690         OO.ui.ButtonWidget.super.call( this, config );
9692         // Mixin constructors
9693         OO.ui.ButtonElement.call( this, config );
9694         OO.ui.IconElement.call( this, config );
9695         OO.ui.IndicatorElement.call( this, config );
9696         OO.ui.LabelElement.call( this, config );
9697         OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
9698         OO.ui.FlaggedElement.call( this, config );
9699         OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
9701         // Properties
9702         this.href = null;
9703         this.target = null;
9704         this.noFollow = false;
9705         this.isHyperlink = false;
9707         // Initialization
9708         this.$button.append( this.$icon, this.$label, this.$indicator );
9709         this.$element
9710                 .addClass( 'oo-ui-buttonWidget' )
9711                 .append( this.$button );
9712         this.setHref( config.href );
9713         this.setTarget( config.target );
9714         this.setNoFollow( config.noFollow );
9717 /* Setup */
9719 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
9720 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement );
9721 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement );
9722 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement );
9723 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
9724 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
9725 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
9726 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement );
9728 /* Methods */
9731  * @inheritdoc
9732  */
9733 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
9734         if ( !this.isDisabled() ) {
9735                 // Remove the tab-index while the button is down to prevent the button from stealing focus
9736                 this.$button.removeAttr( 'tabindex' );
9737         }
9739         return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e );
9743  * @inheritdoc
9744  */
9745 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
9746         if ( !this.isDisabled() ) {
9747                 // Restore the tab-index after the button is up to restore the button's accessibility
9748                 this.$button.attr( 'tabindex', this.tabIndex );
9749         }
9751         return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e );
9755  * @inheritdoc
9756  */
9757 OO.ui.ButtonWidget.prototype.onClick = function ( e ) {
9758         var ret = OO.ui.ButtonElement.prototype.onClick.call( this, e );
9759         if ( this.isHyperlink ) {
9760                 return true;
9761         }
9762         return ret;
9766  * @inheritdoc
9767  */
9768 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
9769         var ret = OO.ui.ButtonElement.prototype.onKeyPress.call( this, e );
9770         if ( this.isHyperlink ) {
9771                 return true;
9772         }
9773         return ret;
9777  * Get hyperlink location.
9779  * @return {string} Hyperlink location
9780  */
9781 OO.ui.ButtonWidget.prototype.getHref = function () {
9782         return this.href;
9786  * Get hyperlink target.
9788  * @return {string} Hyperlink target
9789  */
9790 OO.ui.ButtonWidget.prototype.getTarget = function () {
9791         return this.target;
9795  * Get search engine traversal hint.
9797  * @return {boolean} Whether search engines should avoid traversing this hyperlink
9798  */
9799 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
9800         return this.noFollow;
9804  * Set hyperlink location.
9806  * @param {string|null} href Hyperlink location, null to remove
9807  */
9808 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
9809         href = typeof href === 'string' ? href : null;
9811         if ( href !== this.href ) {
9812                 this.href = href;
9813                 if ( href !== null ) {
9814                         this.$button.attr( 'href', href );
9815                         this.isHyperlink = true;
9816                 } else {
9817                         this.$button.removeAttr( 'href' );
9818                         this.isHyperlink = false;
9819                 }
9820         }
9822         return this;
9826  * Set hyperlink target.
9828  * @param {string|null} target Hyperlink target, null to remove
9829  */
9830 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
9831         target = typeof target === 'string' ? target : null;
9833         if ( target !== this.target ) {
9834                 this.target = target;
9835                 if ( target !== null ) {
9836                         this.$button.attr( 'target', target );
9837                 } else {
9838                         this.$button.removeAttr( 'target' );
9839                 }
9840         }
9842         return this;
9846  * Set search engine traversal hint.
9848  * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
9849  */
9850 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
9851         noFollow = typeof noFollow === 'boolean' ? noFollow : true;
9853         if ( noFollow !== this.noFollow ) {
9854                 this.noFollow = noFollow;
9855                 if ( noFollow ) {
9856                         this.$button.attr( 'rel', 'nofollow' );
9857                 } else {
9858                         this.$button.removeAttr( 'rel' );
9859                 }
9860         }
9862         return this;
9866  * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
9867  * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
9868  * of the actions. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9869  * and examples.
9871  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
9873  * @class
9874  * @extends OO.ui.ButtonWidget
9875  * @mixins OO.ui.PendingElement
9877  * @constructor
9878  * @param {Object} [config] Configuration options
9879  * @cfg {string} [action] Symbolic action name
9880  * @cfg {string[]} [modes] Symbolic mode names
9881  * @cfg {boolean} [framed=false] Render button with a frame
9882  */
9883 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
9884         // Configuration initialization
9885         config = $.extend( { framed: false }, config );
9887         // Parent constructor
9888         OO.ui.ActionWidget.super.call( this, config );
9890         // Mixin constructors
9891         OO.ui.PendingElement.call( this, config );
9893         // Properties
9894         this.action = config.action || '';
9895         this.modes = config.modes || [];
9896         this.width = 0;
9897         this.height = 0;
9899         // Initialization
9900         this.$element.addClass( 'oo-ui-actionWidget' );
9903 /* Setup */
9905 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
9906 OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement );
9908 /* Events */
9911  * @event resize
9912  */
9914 /* Methods */
9917  * Check if action is available in a certain mode.
9919  * @param {string} mode Name of mode
9920  * @return {boolean} Has mode
9921  */
9922 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
9923         return this.modes.indexOf( mode ) !== -1;
9927  * Get symbolic action name.
9929  * @return {string}
9930  */
9931 OO.ui.ActionWidget.prototype.getAction = function () {
9932         return this.action;
9936  * Get symbolic action name.
9938  * @return {string}
9939  */
9940 OO.ui.ActionWidget.prototype.getModes = function () {
9941         return this.modes.slice();
9945  * Emit a resize event if the size has changed.
9947  * @chainable
9948  */
9949 OO.ui.ActionWidget.prototype.propagateResize = function () {
9950         var width, height;
9952         if ( this.isElementAttached() ) {
9953                 width = this.$element.width();
9954                 height = this.$element.height();
9956                 if ( width !== this.width || height !== this.height ) {
9957                         this.width = width;
9958                         this.height = height;
9959                         this.emit( 'resize' );
9960                 }
9961         }
9963         return this;
9967  * @inheritdoc
9968  */
9969 OO.ui.ActionWidget.prototype.setIcon = function () {
9970         // Mixin method
9971         OO.ui.IconElement.prototype.setIcon.apply( this, arguments );
9972         this.propagateResize();
9974         return this;
9978  * @inheritdoc
9979  */
9980 OO.ui.ActionWidget.prototype.setLabel = function () {
9981         // Mixin method
9982         OO.ui.LabelElement.prototype.setLabel.apply( this, arguments );
9983         this.propagateResize();
9985         return this;
9989  * @inheritdoc
9990  */
9991 OO.ui.ActionWidget.prototype.setFlags = function () {
9992         // Mixin method
9993         OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments );
9994         this.propagateResize();
9996         return this;
10000  * @inheritdoc
10001  */
10002 OO.ui.ActionWidget.prototype.clearFlags = function () {
10003         // Mixin method
10004         OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments );
10005         this.propagateResize();
10007         return this;
10011  * Toggle visibility of button.
10013  * @param {boolean} [show] Show button, omit to toggle visibility
10014  * @chainable
10015  */
10016 OO.ui.ActionWidget.prototype.toggle = function () {
10017         // Parent method
10018         OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
10019         this.propagateResize();
10021         return this;
10025  * Button that shows and hides a popup.
10027  * @class
10028  * @extends OO.ui.ButtonWidget
10029  * @mixins OO.ui.PopupElement
10031  * @constructor
10032  * @param {Object} [config] Configuration options
10033  */
10034 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
10035         // Parent constructor
10036         OO.ui.PopupButtonWidget.super.call( this, config );
10038         // Mixin constructors
10039         OO.ui.PopupElement.call( this, config );
10041         // Events
10042         this.connect( this, { click: 'onAction' } );
10044         // Initialization
10045         this.$element
10046                 .addClass( 'oo-ui-popupButtonWidget' )
10047                 .attr( 'aria-haspopup', 'true' )
10048                 .append( this.popup.$element );
10051 /* Setup */
10053 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
10054 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
10056 /* Methods */
10059  * Handle the button action being triggered.
10060  */
10061 OO.ui.PopupButtonWidget.prototype.onAction = function () {
10062         this.popup.toggle();
10066  * Button that toggles on and off.
10068  * @class
10069  * @extends OO.ui.ButtonWidget
10070  * @mixins OO.ui.ToggleWidget
10072  * @constructor
10073  * @param {Object} [config] Configuration options
10074  * @cfg {boolean} [value=false] Initial value
10075  */
10076 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
10077         // Configuration initialization
10078         config = config || {};
10080         // Parent constructor
10081         OO.ui.ToggleButtonWidget.super.call( this, config );
10083         // Mixin constructors
10084         OO.ui.ToggleWidget.call( this, config );
10086         // Events
10087         this.connect( this, { click: 'onAction' } );
10089         // Initialization
10090         this.$element.addClass( 'oo-ui-toggleButtonWidget' );
10093 /* Setup */
10095 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
10096 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
10098 /* Methods */
10101  * Handle the button action being triggered.
10102  */
10103 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
10104         this.setValue( !this.value );
10108  * @inheritdoc
10109  */
10110 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
10111         value = !!value;
10112         if ( value !== this.value ) {
10113                 this.$button.attr( 'aria-pressed', value.toString() );
10114                 this.setActive( value );
10115         }
10117         // Parent method (from mixin)
10118         OO.ui.ToggleWidget.prototype.setValue.call( this, value );
10120         return this;
10124  * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
10125  * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
10126  * users can interact with it.
10128  *     @example
10129  *     // Example: A DropdownWidget with a menu that contains three options
10130  *     var dropDown=new OO.ui.DropdownWidget( {
10131  *         label: 'Dropdown menu: Select a menu option',
10132  *         menu: {
10133  *             items: [
10134  *                 new OO.ui.MenuOptionWidget( {
10135  *                     data: 'a',
10136  *                     label: 'First'
10137  *                 } ),
10138  *                 new OO.ui.MenuOptionWidget( {
10139  *                     data: 'b',
10140  *                     label: 'Second'
10141  *                 } ),
10142  *                 new OO.ui.MenuOptionWidget( {
10143  *                     data: 'c',
10144  *                     label: 'Third'
10145  *                 } )
10146  *             ]
10147  *         }
10148  *     } );
10150  *     $('body').append(dropDown.$element);
10152  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
10154  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10156  * @class
10157  * @extends OO.ui.Widget
10158  * @mixins OO.ui.IconElement
10159  * @mixins OO.ui.IndicatorElement
10160  * @mixins OO.ui.LabelElement
10161  * @mixins OO.ui.TitledElement
10162  * @mixins OO.ui.TabIndexedElement
10164  * @constructor
10165  * @param {Object} [config] Configuration options
10166  * @cfg {Object} [menu] Configuration options to pass to menu widget
10167  */
10168 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
10169         // Configuration initialization
10170         config = $.extend( { indicator: 'down' }, config );
10172         // Parent constructor
10173         OO.ui.DropdownWidget.super.call( this, config );
10175         // Properties (must be set before TabIndexedElement constructor call)
10176         this.$handle = this.$( '<span>' );
10178         // Mixin constructors
10179         OO.ui.IconElement.call( this, config );
10180         OO.ui.IndicatorElement.call( this, config );
10181         OO.ui.LabelElement.call( this, config );
10182         OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
10183         OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
10185         // Properties
10186         this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
10188         // Events
10189         this.$handle.on( {
10190                 click: this.onClick.bind( this ),
10191                 keypress: this.onKeyPress.bind( this )
10192         } );
10193         this.menu.connect( this, { select: 'onMenuSelect' } );
10195         // Initialization
10196         this.$handle
10197                 .addClass( 'oo-ui-dropdownWidget-handle' )
10198                 .append( this.$icon, this.$label, this.$indicator );
10199         this.$element
10200                 .addClass( 'oo-ui-dropdownWidget' )
10201                 .append( this.$handle, this.menu.$element );
10204 /* Setup */
10206 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
10207 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
10208 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
10209 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
10210 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
10211 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TabIndexedElement );
10213 /* Methods */
10216  * Get the menu.
10218  * @return {OO.ui.MenuSelectWidget} Menu of widget
10219  */
10220 OO.ui.DropdownWidget.prototype.getMenu = function () {
10221         return this.menu;
10225  * Handles menu select events.
10227  * @private
10228  * @param {OO.ui.MenuOptionWidget} item Selected menu item
10229  */
10230 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
10231         var selectedLabel;
10233         if ( !item ) {
10234                 return;
10235         }
10237         selectedLabel = item.getLabel();
10239         // If the label is a DOM element, clone it, because setLabel will append() it
10240         if ( selectedLabel instanceof jQuery ) {
10241                 selectedLabel = selectedLabel.clone();
10242         }
10244         this.setLabel( selectedLabel );
10248  * Handle mouse click events.
10250  * @private
10251  * @param {jQuery.Event} e Mouse click event
10252  */
10253 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
10254         if ( !this.isDisabled() && e.which === 1 ) {
10255                 this.menu.toggle();
10256         }
10257         return false;
10261  * Handle key press events.
10263  * @private
10264  * @param {jQuery.Event} e Key press event
10265  */
10266 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
10267         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
10268                 this.menu.toggle();
10269         }
10270         return false;
10274  * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
10275  * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
10276  * for a list of icons included in the library.
10278  *     @example
10279  *     // An icon widget with a label
10280  *     var myIcon = new OO.ui.IconWidget({
10281  *         icon: 'help',
10282  *         iconTitle: 'Help'
10283  *      });
10284  *      // Create a label.
10285  *      var iconLabel = new OO.ui.LabelWidget({
10286  *          label: 'Help'
10287  *      });
10288  *      $('body').append(myIcon.$element, iconLabel.$element);
10290  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
10292  * @class
10293  * @extends OO.ui.Widget
10294  * @mixins OO.ui.IconElement
10295  * @mixins OO.ui.TitledElement
10297  * @constructor
10298  * @param {Object} [config] Configuration options
10299  */
10300 OO.ui.IconWidget = function OoUiIconWidget( config ) {
10301         // Configuration initialization
10302         config = config || {};
10304         // Parent constructor
10305         OO.ui.IconWidget.super.call( this, config );
10307         // Mixin constructors
10308         OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
10309         OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10311         // Initialization
10312         this.$element.addClass( 'oo-ui-iconWidget' );
10315 /* Setup */
10317 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
10318 OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
10319 OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
10321 /* Static Properties */
10323 OO.ui.IconWidget.static.tagName = 'span';
10326  * Indicator widget.
10328  * See OO.ui.IndicatorElement for more information.
10330  * @class
10331  * @extends OO.ui.Widget
10332  * @mixins OO.ui.IndicatorElement
10333  * @mixins OO.ui.TitledElement
10335  * @constructor
10336  * @param {Object} [config] Configuration options
10337  */
10338 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
10339         // Configuration initialization
10340         config = config || {};
10342         // Parent constructor
10343         OO.ui.IndicatorWidget.super.call( this, config );
10345         // Mixin constructors
10346         OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
10347         OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10349         // Initialization
10350         this.$element.addClass( 'oo-ui-indicatorWidget' );
10353 /* Setup */
10355 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
10356 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
10357 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
10359 /* Static Properties */
10361 OO.ui.IndicatorWidget.static.tagName = 'span';
10364  * InputWidget is the base class for all input widgets, which
10365  * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
10366  * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
10367  * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10369  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10371  * @abstract
10372  * @class
10373  * @extends OO.ui.Widget
10374  * @mixins OO.ui.FlaggedElement
10375  * @mixins OO.ui.TabIndexedElement
10377  * @constructor
10378  * @param {Object} [config] Configuration options
10379  * @cfg {string} [name=''] HTML input name
10380  * @cfg {string} [value=''] Input value
10381  * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
10382  */
10383 OO.ui.InputWidget = function OoUiInputWidget( config ) {
10384         // Configuration initialization
10385         config = config || {};
10387         // Parent constructor
10388         OO.ui.InputWidget.super.call( this, config );
10390         // Properties
10391         this.$input = this.getInputElement( config );
10392         this.value = '';
10393         this.inputFilter = config.inputFilter;
10395         // Mixin constructors
10396         OO.ui.FlaggedElement.call( this, config );
10397         OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
10399         // Events
10400         this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
10402         // Initialization
10403         this.$input
10404                 .attr( 'name', config.name )
10405                 .prop( 'disabled', this.isDisabled() );
10406         this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
10407         this.setValue( config.value );
10410 /* Setup */
10412 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
10413 OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
10414 OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement );
10416 /* Events */
10419  * @event change
10420  * @param {string} value
10421  */
10423 /* Methods */
10426  * Get input element.
10428  * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
10429  * different circumstances. The element must have a `value` property (like form elements).
10431  * @private
10432  * @param {Object} config Configuration options
10433  * @return {jQuery} Input element
10434  */
10435 OO.ui.InputWidget.prototype.getInputElement = function () {
10436         return $( '<input>' );
10440  * Handle potentially value-changing events.
10442  * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
10443  */
10444 OO.ui.InputWidget.prototype.onEdit = function () {
10445         var widget = this;
10446         if ( !this.isDisabled() ) {
10447                 // Allow the stack to clear so the value will be updated
10448                 setTimeout( function () {
10449                         widget.setValue( widget.$input.val() );
10450                 } );
10451         }
10455  * Get the value of the input.
10457  * @return {string} Input value
10458  */
10459 OO.ui.InputWidget.prototype.getValue = function () {
10460         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10461         // it, and we won't know unless they're kind enough to trigger a 'change' event.
10462         var value = this.$input.val();
10463         if ( this.value !== value ) {
10464                 this.setValue( value );
10465         }
10466         return this.value;
10470  * Sets the direction of the current input, either RTL or LTR
10472  * @param {boolean} isRTL
10473  */
10474 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
10475         this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
10479  * Set the value of the input.
10481  * @param {string} value New value
10482  * @fires change
10483  * @chainable
10484  */
10485 OO.ui.InputWidget.prototype.setValue = function ( value ) {
10486         value = this.cleanUpValue( value );
10487         // Update the DOM if it has changed. Note that with cleanUpValue, it
10488         // is possible for the DOM value to change without this.value changing.
10489         if ( this.$input.val() !== value ) {
10490                 this.$input.val( value );
10491         }
10492         if ( this.value !== value ) {
10493                 this.value = value;
10494                 this.emit( 'change', this.value );
10495         }
10496         return this;
10500  * Clean up incoming value.
10502  * Ensures value is a string, and converts undefined and null to empty string.
10504  * @private
10505  * @param {string} value Original value
10506  * @return {string} Cleaned up value
10507  */
10508 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
10509         if ( value === undefined || value === null ) {
10510                 return '';
10511         } else if ( this.inputFilter ) {
10512                 return this.inputFilter( String( value ) );
10513         } else {
10514                 return String( value );
10515         }
10519  * Simulate the behavior of clicking on a label bound to this input.
10520  */
10521 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
10522         if ( !this.isDisabled() ) {
10523                 if ( this.$input.is( ':checkbox,:radio' ) ) {
10524                         this.$input.click();
10525                 } else if ( this.$input.is( ':input' ) ) {
10526                         this.$input[ 0 ].focus();
10527                 }
10528         }
10532  * @inheritdoc
10533  */
10534 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
10535         OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
10536         if ( this.$input ) {
10537                 this.$input.prop( 'disabled', this.isDisabled() );
10538         }
10539         return this;
10543  * Focus the input.
10545  * @chainable
10546  */
10547 OO.ui.InputWidget.prototype.focus = function () {
10548         this.$input[ 0 ].focus();
10549         return this;
10553  * Blur the input.
10555  * @chainable
10556  */
10557 OO.ui.InputWidget.prototype.blur = function () {
10558         this.$input[ 0 ].blur();
10559         return this;
10563  * ButtonInputWidget is used to submit HTML forms and is intended to be used within
10564  * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
10565  * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
10566  * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
10567  * [OOjs UI documentation on MediaWiki] [1] for more information.
10569  *     @example
10570  *     // A ButtonInputWidget rendered as an HTML button, the default.
10571  *     var button = new OO.ui.ButtonInputWidget( {
10572  *         label: 'Input button',
10573  *         icon: 'check',
10574  *         value: 'check'
10575  *     } );
10576  *     $( 'body' ).append( button.$element );
10578  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
10580  * @class
10581  * @extends OO.ui.InputWidget
10582  * @mixins OO.ui.ButtonElement
10583  * @mixins OO.ui.IconElement
10584  * @mixins OO.ui.IndicatorElement
10585  * @mixins OO.ui.LabelElement
10586  * @mixins OO.ui.TitledElement
10587  * @mixins OO.ui.FlaggedElement
10589  * @constructor
10590  * @param {Object} [config] Configuration options
10591  * @cfg {string} [type='button'] HTML tag `type` attribute, may be 'button', 'submit' or 'reset'
10592  * @cfg {boolean} [useInputTag=false] Whether to use `<input/>` rather than `<button/>`. Only useful
10593  *  if you need IE 6 support in a form with multiple buttons. If you use this option, icons and
10594  *  indicators will not be displayed, it won't be possible to have a non-plaintext label, and it
10595  *  won't be possible to set a value (which will internally become identical to the label).
10596  */
10597 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
10598         // Configuration initialization
10599         config = $.extend( { type: 'button', useInputTag: false }, config );
10601         // Properties (must be set before parent constructor, which calls #setValue)
10602         this.useInputTag = config.useInputTag;
10604         // Parent constructor
10605         OO.ui.ButtonInputWidget.super.call( this, config );
10607         // Mixin constructors
10608         OO.ui.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
10609         OO.ui.IconElement.call( this, config );
10610         OO.ui.IndicatorElement.call( this, config );
10611         OO.ui.LabelElement.call( this, config );
10612         OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
10613         OO.ui.FlaggedElement.call( this, config );
10615         // Initialization
10616         if ( !config.useInputTag ) {
10617                 this.$input.append( this.$icon, this.$label, this.$indicator );
10618         }
10619         this.$element.addClass( 'oo-ui-buttonInputWidget' );
10622 /* Setup */
10624 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
10625 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.ButtonElement );
10626 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IconElement );
10627 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IndicatorElement );
10628 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement );
10629 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
10630 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
10632 /* Methods */
10635  * @inheritdoc
10636  * @private
10637  */
10638 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
10639         var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
10640         return $( html );
10644  * Set label value.
10646  * Overridden to support setting the 'value' of `<input/>` elements.
10648  * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
10649  *  text; or null for no label
10650  * @chainable
10651  */
10652 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
10653         OO.ui.LabelElement.prototype.setLabel.call( this, label );
10655         if ( this.useInputTag ) {
10656                 if ( typeof label === 'function' ) {
10657                         label = OO.ui.resolveMsg( label );
10658                 }
10659                 if ( label instanceof jQuery ) {
10660                         label = label.text();
10661                 }
10662                 if ( !label ) {
10663                         label = '';
10664                 }
10665                 this.$input.val( label );
10666         }
10668         return this;
10672  * Set the value of the input.
10674  * Overridden to disable for `<input/>` elements, which have value identical to the label.
10676  * @param {string} value New value
10677  * @chainable
10678  */
10679 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
10680         if ( !this.useInputTag ) {
10681                 OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
10682         }
10683         return this;
10687  * Checkbox input widget.
10689  * @class
10690  * @extends OO.ui.InputWidget
10692  * @constructor
10693  * @param {Object} [config] Configuration options
10694  * @cfg {boolean} [selected=false] Whether the checkbox is initially selected
10695  */
10696 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
10697         // Configuration initialization
10698         config = config || {};
10700         // Parent constructor
10701         OO.ui.CheckboxInputWidget.super.call( this, config );
10703         // Initialization
10704         this.$element.addClass( 'oo-ui-checkboxInputWidget' );
10705         this.setSelected( config.selected !== undefined ? config.selected : false );
10708 /* Setup */
10710 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
10712 /* Methods */
10715  * @inheritdoc
10716  * @private
10717  */
10718 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
10719         return $( '<input type="checkbox" />' );
10723  * @inheritdoc
10724  */
10725 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
10726         var widget = this;
10727         if ( !this.isDisabled() ) {
10728                 // Allow the stack to clear so the value will be updated
10729                 setTimeout( function () {
10730                         widget.setSelected( widget.$input.prop( 'checked' ) );
10731                 } );
10732         }
10736  * Set selection state of this checkbox.
10738  * @param {boolean} state Whether the checkbox is selected
10739  * @chainable
10740  */
10741 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
10742         state = !!state;
10743         if ( this.selected !== state ) {
10744                 this.selected = state;
10745                 this.$input.prop( 'checked', this.selected );
10746                 this.emit( 'change', this.selected );
10747         }
10748         return this;
10752  * Check if this checkbox is selected.
10754  * @return {boolean} Checkbox is selected
10755  */
10756 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
10757         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10758         // it, and we won't know unless they're kind enough to trigger a 'change' event.
10759         var selected = this.$input.prop( 'checked' );
10760         if ( this.selected !== selected ) {
10761                 this.setSelected( selected );
10762         }
10763         return this.selected;
10767  * A OO.ui.DropdownWidget synchronized with a `<input type=hidden>` for form submission. Intended to
10768  * be used within a OO.ui.FormLayout.
10770  * @class
10771  * @extends OO.ui.InputWidget
10773  * @constructor
10774  * @param {Object} [config] Configuration options
10775  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10776  */
10777 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
10778         // Configuration initialization
10779         config = config || {};
10781         // Properties (must be done before parent constructor which calls #setDisabled)
10782         this.dropdownWidget = new OO.ui.DropdownWidget();
10784         // Parent constructor
10785         OO.ui.DropdownInputWidget.super.call( this, config );
10787         // Events
10788         this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
10790         // Initialization
10791         this.setOptions( config.options || [] );
10792         this.$element
10793                 .addClass( 'oo-ui-dropdownInputWidget' )
10794                 .append( this.dropdownWidget.$element );
10797 /* Setup */
10799 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
10801 /* Methods */
10804  * @inheritdoc
10805  * @private
10806  */
10807 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
10808         return $( '<input type="hidden">' );
10812  * Handles menu select events.
10814  * @param {OO.ui.MenuOptionWidget} item Selected menu item
10815  */
10816 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
10817         this.setValue( item.getData() );
10821  * @inheritdoc
10822  */
10823 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
10824         var item = this.dropdownWidget.getMenu().getItemFromData( value );
10825         if ( item ) {
10826                 this.dropdownWidget.getMenu().selectItem( item );
10827         }
10828         OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
10829         return this;
10833  * @inheritdoc
10834  */
10835 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
10836         this.dropdownWidget.setDisabled( state );
10837         OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
10838         return this;
10842  * Set the options available for this input.
10844  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10845  * @chainable
10846  */
10847 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
10848         var value = this.getValue();
10850         // Rebuild the dropdown menu
10851         this.dropdownWidget.getMenu()
10852                 .clearItems()
10853                 .addItems( options.map( function ( opt ) {
10854                         return new OO.ui.MenuOptionWidget( {
10855                                 data: opt.data,
10856                                 label: opt.label !== undefined ? opt.label : opt.data
10857                         } );
10858                 } ) );
10860         // Restore the previous value, or reset to something sensible
10861         if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
10862                 // Previous value is still available, ensure consistency with the dropdown
10863                 this.setValue( value );
10864         } else {
10865                 // No longer valid, reset
10866                 if ( options.length ) {
10867                         this.setValue( options[ 0 ].data );
10868                 }
10869         }
10871         return this;
10875  * @inheritdoc
10876  */
10877 OO.ui.DropdownInputWidget.prototype.focus = function () {
10878         this.dropdownWidget.getMenu().toggle( true );
10879         return this;
10883  * @inheritdoc
10884  */
10885 OO.ui.DropdownInputWidget.prototype.blur = function () {
10886         this.dropdownWidget.getMenu().toggle( false );
10887         return this;
10891  * Radio input widget.
10893  * Radio buttons only make sense as a set, and you probably want to use the OO.ui.RadioSelectWidget
10894  * class instead of using this class directly.
10896  * @class
10897  * @extends OO.ui.InputWidget
10899  * @constructor
10900  * @param {Object} [config] Configuration options
10901  * @cfg {boolean} [selected=false] Whether the radio button is initially selected
10902  */
10903 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
10904         // Configuration initialization
10905         config = config || {};
10907         // Parent constructor
10908         OO.ui.RadioInputWidget.super.call( this, config );
10910         // Initialization
10911         this.$element.addClass( 'oo-ui-radioInputWidget' );
10912         this.setSelected( config.selected !== undefined ? config.selected : false );
10915 /* Setup */
10917 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
10919 /* Methods */
10922  * @inheritdoc
10923  * @private
10924  */
10925 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
10926         return $( '<input type="radio" />' );
10930  * @inheritdoc
10931  */
10932 OO.ui.RadioInputWidget.prototype.onEdit = function () {
10933         // RadioInputWidget doesn't track its state.
10937  * Set selection state of this radio button.
10939  * @param {boolean} state Whether the button is selected
10940  * @chainable
10941  */
10942 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
10943         // RadioInputWidget doesn't track its state.
10944         this.$input.prop( 'checked', state );
10945         return this;
10949  * Check if this radio button is selected.
10951  * @return {boolean} Radio is selected
10952  */
10953 OO.ui.RadioInputWidget.prototype.isSelected = function () {
10954         return this.$input.prop( 'checked' );
10958  * Input widget with a text field.
10960  * @class
10961  * @extends OO.ui.InputWidget
10962  * @mixins OO.ui.IconElement
10963  * @mixins OO.ui.IndicatorElement
10964  * @mixins OO.ui.PendingElement
10965  * @mixins OO.ui.LabelElement
10967  * @constructor
10968  * @param {Object} [config] Configuration options
10969  * @cfg {string} [type='text'] HTML tag `type` attribute
10970  * @cfg {string} [placeholder] Placeholder text
10971  * @cfg {boolean} [autofocus=false] Ask the browser to focus this widget, using the 'autofocus' HTML
10972  *  attribute
10973  * @cfg {boolean} [readOnly=false] Prevent changes
10974  * @cfg {number} [maxLength] Maximum allowed number of characters to input
10975  * @cfg {boolean} [multiline=false] Allow multiple lines of text
10976  * @cfg {boolean} [autosize=false] Automatically resize to fit content
10977  * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
10978  * @cfg {string} [labelPosition='after'] Label position, 'before' or 'after'
10979  * @cfg {boolean} [required=false] Mark the field as required
10980  * @cfg {RegExp|string} [validate] Regular expression to validate against (or symbolic name referencing
10981  *  one, see #static-validationPatterns)
10982  */
10983 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
10984         // Configuration initialization
10985         config = $.extend( {
10986                 type: 'text',
10987                 labelPosition: 'after',
10988                 maxRows: 10
10989         }, config );
10991         // Parent constructor
10992         OO.ui.TextInputWidget.super.call( this, config );
10994         // Mixin constructors
10995         OO.ui.IconElement.call( this, config );
10996         OO.ui.IndicatorElement.call( this, config );
10997         OO.ui.PendingElement.call( this, config );
10998         OO.ui.LabelElement.call( this, config );
11000         // Properties
11001         this.readOnly = false;
11002         this.multiline = !!config.multiline;
11003         this.autosize = !!config.autosize;
11004         this.maxRows = config.maxRows;
11005         this.validate = null;
11007         // Clone for resizing
11008         if ( this.autosize ) {
11009                 this.$clone = this.$input
11010                         .clone()
11011                         .insertAfter( this.$input )
11012                         .attr( 'aria-hidden', 'true' )
11013                         .addClass( 'oo-ui-element-hidden' );
11014         }
11016         this.setValidation( config.validate );
11017         this.setPosition( config.labelPosition );
11019         // Events
11020         this.$input.on( {
11021                 keypress: this.onKeyPress.bind( this ),
11022                 blur: this.setValidityFlag.bind( this )
11023         } );
11024         this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
11025         this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
11026         this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
11027         this.on( 'labelChange', this.updatePosition.bind( this ) );
11029         // Initialization
11030         this.$element
11031                 .addClass( 'oo-ui-textInputWidget' )
11032                 .append( this.$icon, this.$indicator );
11033         this.setReadOnly( !!config.readOnly );
11034         if ( config.placeholder ) {
11035                 this.$input.attr( 'placeholder', config.placeholder );
11036         }
11037         if ( config.maxLength !== undefined ) {
11038                 this.$input.attr( 'maxlength', config.maxLength );
11039         }
11040         if ( config.autofocus ) {
11041                 this.$input.attr( 'autofocus', 'autofocus' );
11042         }
11043         if ( config.required ) {
11044                 this.$input.attr( 'required', 'true' );
11045         }
11048 /* Setup */
11050 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
11051 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
11052 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
11053 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
11054 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.LabelElement );
11056 /* Static properties */
11058 OO.ui.TextInputWidget.static.validationPatterns = {
11059         'non-empty': /.+/,
11060         integer: /^\d+$/
11063 /* Events */
11066  * User presses enter inside the text box.
11068  * Not called if input is multiline.
11070  * @event enter
11071  */
11074  * User clicks the icon.
11076  * @deprecated Fundamentally not accessible. Make the icon focusable, associate a label or tooltip,
11077  *  and handle click/keypress events on it manually.
11078  * @event icon
11079  */
11082  * User clicks the indicator.
11084  * @deprecated Fundamentally not accessible. Make the indicator focusable, associate a label or
11085  *  tooltip, and handle click/keypress events on it manually.
11086  * @event indicator
11087  */
11089 /* Methods */
11092  * Handle icon mouse down events.
11094  * @param {jQuery.Event} e Mouse down event
11095  * @fires icon
11096  */
11097 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
11098         if ( e.which === 1 ) {
11099                 this.$input[ 0 ].focus();
11100                 this.emit( 'icon' );
11101                 return false;
11102         }
11106  * Handle indicator mouse down events.
11108  * @param {jQuery.Event} e Mouse down event
11109  * @fires indicator
11110  */
11111 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
11112         if ( e.which === 1 ) {
11113                 this.$input[ 0 ].focus();
11114                 this.emit( 'indicator' );
11115                 return false;
11116         }
11120  * Handle key press events.
11122  * @param {jQuery.Event} e Key press event
11123  * @fires enter If enter key is pressed and input is not multiline
11124  */
11125 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
11126         if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
11127                 this.emit( 'enter', e );
11128         }
11132  * Handle element attach events.
11134  * @param {jQuery.Event} e Element attach event
11135  */
11136 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
11137         // Any previously calculated size is now probably invalid if we reattached elsewhere
11138         this.valCache = null;
11139         this.adjustSize();
11140         this.positionLabel();
11144  * @inheritdoc
11145  */
11146 OO.ui.TextInputWidget.prototype.onEdit = function () {
11147         this.adjustSize();
11149         // Parent method
11150         return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
11154  * @inheritdoc
11155  */
11156 OO.ui.TextInputWidget.prototype.setValue = function ( value ) {
11157         // Parent method
11158         OO.ui.TextInputWidget.super.prototype.setValue.call( this, value );
11160         this.setValidityFlag();
11161         this.adjustSize();
11162         return this;
11166  * Check if the widget is read-only.
11168  * @return {boolean}
11169  */
11170 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
11171         return this.readOnly;
11175  * Set the read-only state of the widget.
11177  * This should probably change the widget's appearance and prevent it from being used.
11179  * @param {boolean} state Make input read-only
11180  * @chainable
11181  */
11182 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
11183         this.readOnly = !!state;
11184         this.$input.prop( 'readOnly', this.readOnly );
11185         return this;
11189  * Automatically adjust the size of the text input.
11191  * This only affects multi-line inputs that are auto-sized.
11193  * @chainable
11194  */
11195 OO.ui.TextInputWidget.prototype.adjustSize = function () {
11196         var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
11198         if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
11199                 this.$clone
11200                         .val( this.$input.val() )
11201                         .attr( 'rows', '' )
11202                         // Set inline height property to 0 to measure scroll height
11203                         .css( 'height', 0 );
11205                 this.$clone.removeClass( 'oo-ui-element-hidden' );
11207                 this.valCache = this.$input.val();
11209                 scrollHeight = this.$clone[ 0 ].scrollHeight;
11211                 // Remove inline height property to measure natural heights
11212                 this.$clone.css( 'height', '' );
11213                 innerHeight = this.$clone.innerHeight();
11214                 outerHeight = this.$clone.outerHeight();
11216                 // Measure max rows height
11217                 this.$clone
11218                         .attr( 'rows', this.maxRows )
11219                         .css( 'height', 'auto' )
11220                         .val( '' );
11221                 maxInnerHeight = this.$clone.innerHeight();
11223                 // Difference between reported innerHeight and scrollHeight with no scrollbars present
11224                 // Equals 1 on Blink-based browsers and 0 everywhere else
11225                 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
11226                 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
11228                 this.$clone.addClass( 'oo-ui-element-hidden' );
11230                 // Only apply inline height when expansion beyond natural height is needed
11231                 if ( idealHeight > innerHeight ) {
11232                         // Use the difference between the inner and outer height as a buffer
11233                         this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
11234                 } else {
11235                         this.$input.css( 'height', '' );
11236                 }
11237         }
11238         return this;
11242  * @inheritdoc
11243  * @private
11244  */
11245 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
11246         return config.multiline ? $( '<textarea>' ) : $( '<input type="' + config.type + '" />' );
11250  * Check if input supports multiple lines.
11252  * @return {boolean}
11253  */
11254 OO.ui.TextInputWidget.prototype.isMultiline = function () {
11255         return !!this.multiline;
11259  * Check if input automatically adjusts its size.
11261  * @return {boolean}
11262  */
11263 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
11264         return !!this.autosize;
11268  * Select the contents of the input.
11270  * @chainable
11271  */
11272 OO.ui.TextInputWidget.prototype.select = function () {
11273         this.$input.select();
11274         return this;
11278  * Sets the validation pattern to use.
11279  * @param {RegExp|string|null} validate Regular expression (or symbolic name referencing
11280  *  one, see #static-validationPatterns)
11281  */
11282 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
11283         if ( validate instanceof RegExp ) {
11284                 this.validate = validate;
11285         } else {
11286                 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
11287         }
11291  * Sets the 'invalid' flag appropriately.
11292  */
11293 OO.ui.TextInputWidget.prototype.setValidityFlag = function () {
11294         var widget = this;
11295         this.isValid().done( function ( valid ) {
11296                 widget.setFlags( { invalid: !valid } );
11297         } );
11301  * Returns whether or not the current value is considered valid, according to the
11302  * supplied validation pattern.
11304  * @return {jQuery.Deferred}
11305  */
11306 OO.ui.TextInputWidget.prototype.isValid = function () {
11307         return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
11311  * Set the position of the inline label.
11313  * @param {string} labelPosition Label position, 'before' or 'after'
11314  * @chainable
11315  */
11316 OO.ui.TextInputWidget.prototype.setPosition = function ( labelPosition ) {
11317         this.labelPosition = labelPosition;
11318         this.updatePosition();
11319         return this;
11323  * Update the position of the inline label.
11325  * @chainable
11326  */
11327 OO.ui.TextInputWidget.prototype.updatePosition = function () {
11328         var after = this.labelPosition === 'after';
11330         this.$element
11331                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
11332                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
11334         if ( this.label ) {
11335                 this.positionLabel();
11336         }
11338         return this;
11342  * Position the label by setting the correct padding on the input.
11344  * @chainable
11345  */
11346 OO.ui.TextInputWidget.prototype.positionLabel = function () {
11347         // Clear old values
11348         this.$input
11349                 // Clear old values if present
11350                 .css( {
11351                         'padding-right': '',
11352                         'padding-left': ''
11353                 } );
11355         if ( this.label ) {
11356                 this.$element.append( this.$label );
11357         } else {
11358                 this.$label.detach();
11359                 return;
11360         }
11362         var after = this.labelPosition === 'after',
11363                 rtl = this.$element.css( 'direction' ) === 'rtl',
11364                 property = after === rtl ? 'padding-left' : 'padding-right';
11366         this.$input.css( property, this.$label.outerWidth( true ) );
11368         return this;
11372  * Text input with a menu of optional values.
11374  * @class
11375  * @extends OO.ui.Widget
11376  * @mixins OO.ui.TabIndexedElement
11378  * @constructor
11379  * @param {Object} [config] Configuration options
11380  * @cfg {Object} [menu] Configuration options to pass to menu widget
11381  * @cfg {Object} [input] Configuration options to pass to input widget
11382  * @cfg {jQuery} [$overlay] Overlay layer; defaults to relative positioning
11383  */
11384 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
11385         // Configuration initialization
11386         config = config || {};
11388         // Parent constructor
11389         OO.ui.ComboBoxWidget.super.call( this, config );
11391         // Properties (must be set before TabIndexedElement constructor call)
11392         this.$indicator = this.$( '<span>' );
11394         // Mixin constructors
11395         OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
11397         // Properties
11398         this.$overlay = config.$overlay || this.$element;
11399         this.input = new OO.ui.TextInputWidget( $.extend(
11400                 {
11401                         indicator: 'down',
11402                         $indicator: this.$indicator,
11403                         disabled: this.isDisabled()
11404                 },
11405                 config.input
11406         ) );
11407         this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
11408                 {
11409                         widget: this,
11410                         input: this.input,
11411                         disabled: this.isDisabled()
11412                 },
11413                 config.menu
11414         ) );
11416         // Events
11417         this.$indicator.on( {
11418                 click: this.onClick.bind( this ),
11419                 keypress: this.onKeyPress.bind( this )
11420         } );
11421         this.input.connect( this, {
11422                 change: 'onInputChange',
11423                 enter: 'onInputEnter'
11424         } );
11425         this.menu.connect( this, {
11426                 choose: 'onMenuChoose',
11427                 add: 'onMenuItemsChange',
11428                 remove: 'onMenuItemsChange'
11429         } );
11431         // Initialization
11432         this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
11433         this.$overlay.append( this.menu.$element );
11434         this.onMenuItemsChange();
11437 /* Setup */
11439 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
11440 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.TabIndexedElement );
11442 /* Methods */
11445  * Get the combobox's menu.
11446  * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
11447  */
11448 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
11449         return this.menu;
11453  * Handle input change events.
11455  * @param {string} value New value
11456  */
11457 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
11458         var match = this.menu.getItemFromData( value );
11460         this.menu.selectItem( match );
11461         if ( this.menu.getHighlightedItem() ) {
11462                 this.menu.highlightItem( match );
11463         }
11465         if ( !this.isDisabled() ) {
11466                 this.menu.toggle( true );
11467         }
11471  * Handle mouse click events.
11473  * @param {jQuery.Event} e Mouse click event
11474  */
11475 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
11476         if ( !this.isDisabled() && e.which === 1 ) {
11477                 this.menu.toggle();
11478                 this.input.$input[ 0 ].focus();
11479         }
11480         return false;
11484  * Handle key press events.
11486  * @param {jQuery.Event} e Key press event
11487  */
11488 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
11489         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
11490                 this.menu.toggle();
11491                 this.input.$input[ 0 ].focus();
11492         }
11493         return false;
11497  * Handle input enter events.
11498  */
11499 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
11500         if ( !this.isDisabled() ) {
11501                 this.menu.toggle( false );
11502         }
11506  * Handle menu choose events.
11508  * @param {OO.ui.OptionWidget} item Chosen item
11509  */
11510 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
11511         if ( item ) {
11512                 this.input.setValue( item.getData() );
11513         }
11517  * Handle menu item change events.
11518  */
11519 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
11520         var match = this.menu.getItemFromData( this.input.getValue() );
11521         this.menu.selectItem( match );
11522         if ( this.menu.getHighlightedItem() ) {
11523                 this.menu.highlightItem( match );
11524         }
11525         this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
11529  * @inheritdoc
11530  */
11531 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
11532         // Parent method
11533         OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
11535         if ( this.input ) {
11536                 this.input.setDisabled( this.isDisabled() );
11537         }
11538         if ( this.menu ) {
11539                 this.menu.setDisabled( this.isDisabled() );
11540         }
11542         return this;
11546  * Label widget.
11548  * @class
11549  * @extends OO.ui.Widget
11550  * @mixins OO.ui.LabelElement
11552  * @constructor
11553  * @param {Object} [config] Configuration options
11554  * @cfg {OO.ui.InputWidget} [input] Input widget this label is for
11555  */
11556 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
11557         // Configuration initialization
11558         config = config || {};
11560         // Parent constructor
11561         OO.ui.LabelWidget.super.call( this, config );
11563         // Mixin constructors
11564         OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
11565         OO.ui.TitledElement.call( this, config );
11567         // Properties
11568         this.input = config.input;
11570         // Events
11571         if ( this.input instanceof OO.ui.InputWidget ) {
11572                 this.$element.on( 'click', this.onClick.bind( this ) );
11573         }
11575         // Initialization
11576         this.$element.addClass( 'oo-ui-labelWidget' );
11579 /* Setup */
11581 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
11582 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
11583 OO.mixinClass( OO.ui.LabelWidget, OO.ui.TitledElement );
11585 /* Static Properties */
11587 OO.ui.LabelWidget.static.tagName = 'span';
11589 /* Methods */
11592  * Handles label mouse click events.
11594  * @param {jQuery.Event} e Mouse click event
11595  */
11596 OO.ui.LabelWidget.prototype.onClick = function () {
11597         this.input.simulateLabelClick();
11598         return false;
11602  * Generic option widget for use with OO.ui.SelectWidget.
11604  * @class
11605  * @extends OO.ui.Widget
11606  * @mixins OO.ui.LabelElement
11607  * @mixins OO.ui.FlaggedElement
11609  * @constructor
11610  * @param {Object} [config] Configuration options
11611  */
11612 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
11613         // Configuration initialization
11614         config = config || {};
11616         // Parent constructor
11617         OO.ui.OptionWidget.super.call( this, config );
11619         // Mixin constructors
11620         OO.ui.ItemWidget.call( this );
11621         OO.ui.LabelElement.call( this, config );
11622         OO.ui.FlaggedElement.call( this, config );
11624         // Properties
11625         this.selected = false;
11626         this.highlighted = false;
11627         this.pressed = false;
11629         // Initialization
11630         this.$element
11631                 .data( 'oo-ui-optionWidget', this )
11632                 .attr( 'role', 'option' )
11633                 .addClass( 'oo-ui-optionWidget' )
11634                 .append( this.$label );
11637 /* Setup */
11639 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
11640 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
11641 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement );
11642 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement );
11644 /* Static Properties */
11646 OO.ui.OptionWidget.static.selectable = true;
11648 OO.ui.OptionWidget.static.highlightable = true;
11650 OO.ui.OptionWidget.static.pressable = true;
11652 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
11654 /* Methods */
11657  * Check if option can be selected.
11659  * @return {boolean} Item is selectable
11660  */
11661 OO.ui.OptionWidget.prototype.isSelectable = function () {
11662         return this.constructor.static.selectable && !this.isDisabled();
11666  * Check if option can be highlighted.
11668  * @return {boolean} Item is highlightable
11669  */
11670 OO.ui.OptionWidget.prototype.isHighlightable = function () {
11671         return this.constructor.static.highlightable && !this.isDisabled();
11675  * Check if option can be pressed.
11677  * @return {boolean} Item is pressable
11678  */
11679 OO.ui.OptionWidget.prototype.isPressable = function () {
11680         return this.constructor.static.pressable && !this.isDisabled();
11684  * Check if option is selected.
11686  * @return {boolean} Item is selected
11687  */
11688 OO.ui.OptionWidget.prototype.isSelected = function () {
11689         return this.selected;
11693  * Check if option is highlighted.
11695  * @return {boolean} Item is highlighted
11696  */
11697 OO.ui.OptionWidget.prototype.isHighlighted = function () {
11698         return this.highlighted;
11702  * Check if option is pressed.
11704  * @return {boolean} Item is pressed
11705  */
11706 OO.ui.OptionWidget.prototype.isPressed = function () {
11707         return this.pressed;
11711  * Set selected state.
11713  * @param {boolean} [state=false] Select option
11714  * @chainable
11715  */
11716 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
11717         if ( this.constructor.static.selectable ) {
11718                 this.selected = !!state;
11719                 this.$element
11720                         .toggleClass( 'oo-ui-optionWidget-selected', state )
11721                         .attr( 'aria-selected', state.toString() );
11722                 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
11723                         this.scrollElementIntoView();
11724                 }
11725                 this.updateThemeClasses();
11726         }
11727         return this;
11731  * Set highlighted state.
11733  * @param {boolean} [state=false] Highlight option
11734  * @chainable
11735  */
11736 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
11737         if ( this.constructor.static.highlightable ) {
11738                 this.highlighted = !!state;
11739                 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
11740                 this.updateThemeClasses();
11741         }
11742         return this;
11746  * Set pressed state.
11748  * @param {boolean} [state=false] Press option
11749  * @chainable
11750  */
11751 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
11752         if ( this.constructor.static.pressable ) {
11753                 this.pressed = !!state;
11754                 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
11755                 this.updateThemeClasses();
11756         }
11757         return this;
11761  * Option widget with an option icon and indicator.
11763  * Use together with OO.ui.SelectWidget.
11765  * @class
11766  * @extends OO.ui.OptionWidget
11767  * @mixins OO.ui.IconElement
11768  * @mixins OO.ui.IndicatorElement
11770  * @constructor
11771  * @param {Object} [config] Configuration options
11772  */
11773 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
11774         // Parent constructor
11775         OO.ui.DecoratedOptionWidget.super.call( this, config );
11777         // Mixin constructors
11778         OO.ui.IconElement.call( this, config );
11779         OO.ui.IndicatorElement.call( this, config );
11781         // Initialization
11782         this.$element
11783                 .addClass( 'oo-ui-decoratedOptionWidget' )
11784                 .prepend( this.$icon )
11785                 .append( this.$indicator );
11788 /* Setup */
11790 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
11791 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement );
11792 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
11795  * Option widget that looks like a button.
11797  * Use together with OO.ui.ButtonSelectWidget.
11799  * @class
11800  * @extends OO.ui.DecoratedOptionWidget
11801  * @mixins OO.ui.ButtonElement
11802  * @mixins OO.ui.TabIndexedElement
11804  * @constructor
11805  * @param {Object} [config] Configuration options
11806  */
11807 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
11808         // Configuration initialization
11809         config = $.extend( { tabIndex: -1 }, config );
11811         // Parent constructor
11812         OO.ui.ButtonOptionWidget.super.call( this, config );
11814         // Mixin constructors
11815         OO.ui.ButtonElement.call( this, config );
11816         OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
11818         // Initialization
11819         this.$element.addClass( 'oo-ui-buttonOptionWidget' );
11820         this.$button.append( this.$element.contents() );
11821         this.$element.append( this.$button );
11824 /* Setup */
11826 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
11827 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
11828 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement );
11830 /* Static Properties */
11832 // Allow button mouse down events to pass through so they can be handled by the parent select widget
11833 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
11835 OO.ui.ButtonOptionWidget.static.highlightable = false;
11837 /* Methods */
11840  * @inheritdoc
11841  */
11842 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
11843         OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
11845         if ( this.constructor.static.selectable ) {
11846                 this.setActive( state );
11847         }
11849         return this;
11853  * Option widget that looks like a radio button.
11855  * Use together with OO.ui.RadioSelectWidget.
11857  * @class
11858  * @extends OO.ui.OptionWidget
11860  * @constructor
11861  * @param {Object} [config] Configuration options
11862  */
11863 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
11864         // Parent constructor
11865         OO.ui.RadioOptionWidget.super.call( this, config );
11867         // Properties
11868         this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
11870         // Initialization
11871         this.$element
11872                 .addClass( 'oo-ui-radioOptionWidget' )
11873                 .prepend( this.radio.$element );
11876 /* Setup */
11878 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
11880 /* Static Properties */
11882 OO.ui.RadioOptionWidget.static.highlightable = false;
11884 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
11886 OO.ui.RadioOptionWidget.static.pressable = false;
11888 OO.ui.RadioOptionWidget.static.tagName = 'label';
11890 /* Methods */
11893  * @inheritdoc
11894  */
11895 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
11896         OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
11898         this.radio.setSelected( state );
11900         return this;
11904  * Item of an OO.ui.MenuSelectWidget.
11906  * @class
11907  * @extends OO.ui.DecoratedOptionWidget
11909  * @constructor
11910  * @param {Object} [config] Configuration options
11911  */
11912 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
11913         // Configuration initialization
11914         config = $.extend( { icon: 'check' }, config );
11916         // Parent constructor
11917         OO.ui.MenuOptionWidget.super.call( this, config );
11919         // Initialization
11920         this.$element
11921                 .attr( 'role', 'menuitem' )
11922                 .addClass( 'oo-ui-menuOptionWidget' );
11925 /* Setup */
11927 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
11929 /* Static Properties */
11931 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
11934  * Section to group one or more items in a OO.ui.MenuSelectWidget.
11936  * @class
11937  * @extends OO.ui.DecoratedOptionWidget
11939  * @constructor
11940  * @param {Object} [config] Configuration options
11941  */
11942 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
11943         // Parent constructor
11944         OO.ui.MenuSectionOptionWidget.super.call( this, config );
11946         // Initialization
11947         this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
11950 /* Setup */
11952 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
11954 /* Static Properties */
11956 OO.ui.MenuSectionOptionWidget.static.selectable = false;
11958 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
11961  * Items for an OO.ui.OutlineSelectWidget.
11963  * @class
11964  * @extends OO.ui.DecoratedOptionWidget
11966  * @constructor
11967  * @param {Object} [config] Configuration options
11968  * @cfg {number} [level] Indentation level
11969  * @cfg {boolean} [movable] Allow modification from outline controls
11970  */
11971 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
11972         // Configuration initialization
11973         config = config || {};
11975         // Parent constructor
11976         OO.ui.OutlineOptionWidget.super.call( this, config );
11978         // Properties
11979         this.level = 0;
11980         this.movable = !!config.movable;
11981         this.removable = !!config.removable;
11983         // Initialization
11984         this.$element.addClass( 'oo-ui-outlineOptionWidget' );
11985         this.setLevel( config.level );
11988 /* Setup */
11990 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
11992 /* Static Properties */
11994 OO.ui.OutlineOptionWidget.static.highlightable = false;
11996 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
11998 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
12000 OO.ui.OutlineOptionWidget.static.levels = 3;
12002 /* Methods */
12005  * Check if item is movable.
12007  * Movability is used by outline controls.
12009  * @return {boolean} Item is movable
12010  */
12011 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
12012         return this.movable;
12016  * Check if item is removable.
12018  * Removability is used by outline controls.
12020  * @return {boolean} Item is removable
12021  */
12022 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
12023         return this.removable;
12027  * Get indentation level.
12029  * @return {number} Indentation level
12030  */
12031 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
12032         return this.level;
12036  * Set movability.
12038  * Movability is used by outline controls.
12040  * @param {boolean} movable Item is movable
12041  * @chainable
12042  */
12043 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
12044         this.movable = !!movable;
12045         this.updateThemeClasses();
12046         return this;
12050  * Set removability.
12052  * Removability is used by outline controls.
12054  * @param {boolean} movable Item is removable
12055  * @chainable
12056  */
12057 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
12058         this.removable = !!removable;
12059         this.updateThemeClasses();
12060         return this;
12064  * Set indentation level.
12066  * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
12067  * @chainable
12068  */
12069 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
12070         var levels = this.constructor.static.levels,
12071                 levelClass = this.constructor.static.levelClass,
12072                 i = levels;
12074         this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
12075         while ( i-- ) {
12076                 if ( this.level === i ) {
12077                         this.$element.addClass( levelClass + i );
12078                 } else {
12079                         this.$element.removeClass( levelClass + i );
12080                 }
12081         }
12082         this.updateThemeClasses();
12084         return this;
12088  * Container for content that is overlaid and positioned absolutely.
12090  * @class
12091  * @extends OO.ui.Widget
12092  * @mixins OO.ui.LabelElement
12094  * @constructor
12095  * @param {Object} [config] Configuration options
12096  * @cfg {number} [width=320] Width of popup in pixels
12097  * @cfg {number} [height] Height of popup, omit to use automatic height
12098  * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
12099  * @cfg {string} [align='center'] Alignment of popup to origin
12100  * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
12101  * @cfg {number} [containerPadding=10] How much padding to keep between popup and container
12102  * @cfg {jQuery} [$content] Content to append to the popup's body
12103  * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
12104  * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
12105  * @cfg {boolean} [head] Show label and close button at the top
12106  * @cfg {boolean} [padded] Add padding to the body
12107  */
12108 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
12109         // Configuration initialization
12110         config = config || {};
12112         // Parent constructor
12113         OO.ui.PopupWidget.super.call( this, config );
12115         // Properties (must be set before ClippableElement constructor call)
12116         this.$body = $( '<div>' );
12118         // Mixin constructors
12119         OO.ui.LabelElement.call( this, config );
12120         OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
12122         // Properties
12123         this.$popup = $( '<div>' );
12124         this.$head = $( '<div>' );
12125         this.$anchor = $( '<div>' );
12126         // If undefined, will be computed lazily in updateDimensions()
12127         this.$container = config.$container;
12128         this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
12129         this.autoClose = !!config.autoClose;
12130         this.$autoCloseIgnore = config.$autoCloseIgnore;
12131         this.transitionTimeout = null;
12132         this.anchor = null;
12133         this.width = config.width !== undefined ? config.width : 320;
12134         this.height = config.height !== undefined ? config.height : null;
12135         this.align = config.align || 'center';
12136         this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
12137         this.onMouseDownHandler = this.onMouseDown.bind( this );
12139         // Events
12140         this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
12142         // Initialization
12143         this.toggleAnchor( config.anchor === undefined || config.anchor );
12144         this.$body.addClass( 'oo-ui-popupWidget-body' );
12145         this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
12146         this.$head
12147                 .addClass( 'oo-ui-popupWidget-head' )
12148                 .append( this.$label, this.closeButton.$element );
12149         if ( !config.head ) {
12150                 this.$head.addClass( 'oo-ui-element-hidden' );
12151         }
12152         this.$popup
12153                 .addClass( 'oo-ui-popupWidget-popup' )
12154                 .append( this.$head, this.$body );
12155         this.$element
12156                 .addClass( 'oo-ui-popupWidget' )
12157                 .append( this.$popup, this.$anchor );
12158         // Move content, which was added to #$element by OO.ui.Widget, to the body
12159         if ( config.$content instanceof jQuery ) {
12160                 this.$body.append( config.$content );
12161         }
12162         if ( config.padded ) {
12163                 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
12164         }
12166         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
12167         // that reference properties not initialized at that time of parent class construction
12168         // TODO: Find a better way to handle post-constructor setup
12169         this.visible = false;
12170         this.$element.addClass( 'oo-ui-element-hidden' );
12173 /* Setup */
12175 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
12176 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
12177 OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
12179 /* Methods */
12182  * Handles mouse down events.
12184  * @param {jQuery.Event} e Mouse down event
12185  */
12186 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
12187         if (
12188                 this.isVisible() &&
12189                 !$.contains( this.$element[ 0 ], e.target ) &&
12190                 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
12191         ) {
12192                 this.toggle( false );
12193         }
12197  * Bind mouse down listener.
12198  */
12199 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
12200         // Capture clicks outside popup
12201         this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
12205  * Handles close button click events.
12206  */
12207 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
12208         if ( this.isVisible() ) {
12209                 this.toggle( false );
12210         }
12214  * Unbind mouse down listener.
12215  */
12216 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
12217         this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
12221  * Set whether to show a anchor.
12223  * @param {boolean} [show] Show anchor, omit to toggle
12224  */
12225 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
12226         show = show === undefined ? !this.anchored : !!show;
12228         if ( this.anchored !== show ) {
12229                 if ( show ) {
12230                         this.$element.addClass( 'oo-ui-popupWidget-anchored' );
12231                 } else {
12232                         this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
12233                 }
12234                 this.anchored = show;
12235         }
12239  * Check if showing a anchor.
12241  * @return {boolean} anchor is visible
12242  */
12243 OO.ui.PopupWidget.prototype.hasAnchor = function () {
12244         return this.anchor;
12248  * @inheritdoc
12249  */
12250 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
12251         show = show === undefined ? !this.isVisible() : !!show;
12253         var change = show !== this.isVisible();
12255         // Parent method
12256         OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
12258         if ( change ) {
12259                 if ( show ) {
12260                         if ( this.autoClose ) {
12261                                 this.bindMouseDownListener();
12262                         }
12263                         this.updateDimensions();
12264                         this.toggleClipping( true );
12265                 } else {
12266                         this.toggleClipping( false );
12267                         if ( this.autoClose ) {
12268                                 this.unbindMouseDownListener();
12269                         }
12270                 }
12271         }
12273         return this;
12277  * Set the size of the popup.
12279  * Changing the size may also change the popup's position depending on the alignment.
12281  * @param {number} width Width
12282  * @param {number} height Height
12283  * @param {boolean} [transition=false] Use a smooth transition
12284  * @chainable
12285  */
12286 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
12287         this.width = width;
12288         this.height = height !== undefined ? height : null;
12289         if ( this.isVisible() ) {
12290                 this.updateDimensions( transition );
12291         }
12295  * Update the size and position.
12297  * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
12298  * be called automatically.
12300  * @param {boolean} [transition=false] Use a smooth transition
12301  * @chainable
12302  */
12303 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
12304         var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
12305                 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
12306                 widget = this;
12308         if ( !this.$container ) {
12309                 // Lazy-initialize $container if not specified in constructor
12310                 this.$container = $( this.getClosestScrollableElementContainer() );
12311         }
12313         // Set height and width before measuring things, since it might cause our measurements
12314         // to change (e.g. due to scrollbars appearing or disappearing)
12315         this.$popup.css( {
12316                 width: this.width,
12317                 height: this.height !== null ? this.height : 'auto'
12318         } );
12320         // Compute initial popupOffset based on alignment
12321         popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[ this.align ];
12323         // Figure out if this will cause the popup to go beyond the edge of the container
12324         originOffset = this.$element.offset().left;
12325         containerLeft = this.$container.offset().left;
12326         containerWidth = this.$container.innerWidth();
12327         containerRight = containerLeft + containerWidth;
12328         popupLeft = popupOffset - this.containerPadding;
12329         popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
12330         overlapLeft = ( originOffset + popupLeft ) - containerLeft;
12331         overlapRight = containerRight - ( originOffset + popupRight );
12333         // Adjust offset to make the popup not go beyond the edge, if needed
12334         if ( overlapRight < 0 ) {
12335                 popupOffset += overlapRight;
12336         } else if ( overlapLeft < 0 ) {
12337                 popupOffset -= overlapLeft;
12338         }
12340         // Adjust offset to avoid anchor being rendered too close to the edge
12341         // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
12342         // TODO: Find a measurement that works for CSS anchors and image anchors
12343         anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
12344         if ( popupOffset + this.width < anchorWidth ) {
12345                 popupOffset = anchorWidth - this.width;
12346         } else if ( -popupOffset < anchorWidth ) {
12347                 popupOffset = -anchorWidth;
12348         }
12350         // Prevent transition from being interrupted
12351         clearTimeout( this.transitionTimeout );
12352         if ( transition ) {
12353                 // Enable transition
12354                 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
12355         }
12357         // Position body relative to anchor
12358         this.$popup.css( 'margin-left', popupOffset );
12360         if ( transition ) {
12361                 // Prevent transitioning after transition is complete
12362                 this.transitionTimeout = setTimeout( function () {
12363                         widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
12364                 }, 200 );
12365         } else {
12366                 // Prevent transitioning immediately
12367                 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
12368         }
12370         // Reevaluate clipping state since we've relocated and resized the popup
12371         this.clip();
12373         return this;
12377  * Progress bar widget.
12379  * @class
12380  * @extends OO.ui.Widget
12382  * @constructor
12383  * @param {Object} [config] Configuration options
12384  * @cfg {number|boolean} [progress=false] Initial progress percent or false for indeterminate
12385  */
12386 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
12387         // Configuration initialization
12388         config = config || {};
12390         // Parent constructor
12391         OO.ui.ProgressBarWidget.super.call( this, config );
12393         // Properties
12394         this.$bar = $( '<div>' );
12395         this.progress = null;
12397         // Initialization
12398         this.setProgress( config.progress !== undefined ? config.progress : false );
12399         this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
12400         this.$element
12401                 .attr( {
12402                         role: 'progressbar',
12403                         'aria-valuemin': 0,
12404                         'aria-valuemax': 100
12405                 } )
12406                 .addClass( 'oo-ui-progressBarWidget' )
12407                 .append( this.$bar );
12410 /* Setup */
12412 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
12414 /* Static Properties */
12416 OO.ui.ProgressBarWidget.static.tagName = 'div';
12418 /* Methods */
12421  * Get progress percent
12423  * @return {number} Progress percent
12424  */
12425 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
12426         return this.progress;
12430  * Set progress percent
12432  * @param {number|boolean} progress Progress percent or false for indeterminate
12433  */
12434 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
12435         this.progress = progress;
12437         if ( progress !== false ) {
12438                 this.$bar.css( 'width', this.progress + '%' );
12439                 this.$element.attr( 'aria-valuenow', this.progress );
12440         } else {
12441                 this.$bar.css( 'width', '' );
12442                 this.$element.removeAttr( 'aria-valuenow' );
12443         }
12444         this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
12448  * Search widget.
12450  * Search widgets combine a query input, placed above, and a results selection widget, placed below.
12451  * Results are cleared and populated each time the query is changed.
12453  * @class
12454  * @extends OO.ui.Widget
12456  * @constructor
12457  * @param {Object} [config] Configuration options
12458  * @cfg {string|jQuery} [placeholder] Placeholder text for query input
12459  * @cfg {string} [value] Initial query value
12460  */
12461 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
12462         // Configuration initialization
12463         config = config || {};
12465         // Parent constructor
12466         OO.ui.SearchWidget.super.call( this, config );
12468         // Properties
12469         this.query = new OO.ui.TextInputWidget( {
12470                 icon: 'search',
12471                 placeholder: config.placeholder,
12472                 value: config.value
12473         } );
12474         this.results = new OO.ui.SelectWidget();
12475         this.$query = $( '<div>' );
12476         this.$results = $( '<div>' );
12478         // Events
12479         this.query.connect( this, {
12480                 change: 'onQueryChange',
12481                 enter: 'onQueryEnter'
12482         } );
12483         this.results.connect( this, {
12484                 highlight: 'onResultsHighlight',
12485                 select: 'onResultsSelect'
12486         } );
12487         this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
12489         // Initialization
12490         this.$query
12491                 .addClass( 'oo-ui-searchWidget-query' )
12492                 .append( this.query.$element );
12493         this.$results
12494                 .addClass( 'oo-ui-searchWidget-results' )
12495                 .append( this.results.$element );
12496         this.$element
12497                 .addClass( 'oo-ui-searchWidget' )
12498                 .append( this.$results, this.$query );
12501 /* Setup */
12503 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
12505 /* Events */
12508  * @event highlight
12509  * @param {Object|null} item Item data or null if no item is highlighted
12510  */
12513  * @event select
12514  * @param {Object|null} item Item data or null if no item is selected
12515  */
12517 /* Methods */
12520  * Handle query key down events.
12522  * @param {jQuery.Event} e Key down event
12523  */
12524 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
12525         var highlightedItem, nextItem,
12526                 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
12528         if ( dir ) {
12529                 highlightedItem = this.results.getHighlightedItem();
12530                 if ( !highlightedItem ) {
12531                         highlightedItem = this.results.getSelectedItem();
12532                 }
12533                 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
12534                 this.results.highlightItem( nextItem );
12535                 nextItem.scrollElementIntoView();
12536         }
12540  * Handle select widget select events.
12542  * Clears existing results. Subclasses should repopulate items according to new query.
12544  * @param {string} value New value
12545  */
12546 OO.ui.SearchWidget.prototype.onQueryChange = function () {
12547         // Reset
12548         this.results.clearItems();
12552  * Handle select widget enter key events.
12554  * Selects highlighted item.
12556  * @param {string} value New value
12557  */
12558 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
12559         // Reset
12560         this.results.selectItem( this.results.getHighlightedItem() );
12564  * Handle select widget highlight events.
12566  * @param {OO.ui.OptionWidget} item Highlighted item
12567  * @fires highlight
12568  */
12569 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
12570         this.emit( 'highlight', item ? item.getData() : null );
12574  * Handle select widget select events.
12576  * @param {OO.ui.OptionWidget} item Selected item
12577  * @fires select
12578  */
12579 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
12580         this.emit( 'select', item ? item.getData() : null );
12584  * Get the query input.
12586  * @return {OO.ui.TextInputWidget} Query input
12587  */
12588 OO.ui.SearchWidget.prototype.getQuery = function () {
12589         return this.query;
12593  * Get the results list.
12595  * @return {OO.ui.SelectWidget} Select list
12596  */
12597 OO.ui.SearchWidget.prototype.getResults = function () {
12598         return this.results;
12602  * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
12603  * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
12604  * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
12605  * menu selects}.
12607  * This class should be used together with OO.ui.OptionWidget.
12609  * For more information, please see the [OOjs UI documentation on MediaWiki][1].
12611  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12613  * @class
12614  * @extends OO.ui.Widget
12615  * @mixins OO.ui.GroupElement
12617  * @constructor
12618  * @param {Object} [config] Configuration options
12619  * @cfg {OO.ui.OptionWidget[]} [items] Options to add
12620  */
12621 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
12622         // Configuration initialization
12623         config = config || {};
12625         // Parent constructor
12626         OO.ui.SelectWidget.super.call( this, config );
12628         // Mixin constructors
12629         OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
12631         // Properties
12632         this.pressed = false;
12633         this.selecting = null;
12634         this.onMouseUpHandler = this.onMouseUp.bind( this );
12635         this.onMouseMoveHandler = this.onMouseMove.bind( this );
12636         this.onKeyDownHandler = this.onKeyDown.bind( this );
12638         // Events
12639         this.$element.on( {
12640                 mousedown: this.onMouseDown.bind( this ),
12641                 mouseover: this.onMouseOver.bind( this ),
12642                 mouseleave: this.onMouseLeave.bind( this )
12643         } );
12645         // Initialization
12646         this.$element
12647                 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
12648                 .attr( 'role', 'listbox' );
12649         if ( Array.isArray( config.items ) ) {
12650                 this.addItems( config.items );
12651         }
12654 /* Setup */
12656 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
12658 // Need to mixin base class as well
12659 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
12660 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
12662 /* Events */
12665  * @event highlight
12666  * @param {OO.ui.OptionWidget|null} item Highlighted item
12667  */
12670  * @event press
12671  * @param {OO.ui.OptionWidget|null} item Pressed item
12672  */
12675  * @event select
12676  * @param {OO.ui.OptionWidget|null} item Selected item
12677  */
12680  * @event choose
12681  * @param {OO.ui.OptionWidget|null} item Chosen item
12682  */
12685  * @event add
12686  * @param {OO.ui.OptionWidget[]} items Added items
12687  * @param {number} index Index items were added at
12688  */
12691  * @event remove
12692  * @param {OO.ui.OptionWidget[]} items Removed items
12693  */
12695 /* Methods */
12698  * Handle mouse down events.
12700  * @private
12701  * @param {jQuery.Event} e Mouse down event
12702  */
12703 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
12704         var item;
12706         if ( !this.isDisabled() && e.which === 1 ) {
12707                 this.togglePressed( true );
12708                 item = this.getTargetItem( e );
12709                 if ( item && item.isSelectable() ) {
12710                         this.pressItem( item );
12711                         this.selecting = item;
12712                         this.getElementDocument().addEventListener(
12713                                 'mouseup',
12714                                 this.onMouseUpHandler,
12715                                 true
12716                         );
12717                         this.getElementDocument().addEventListener(
12718                                 'mousemove',
12719                                 this.onMouseMoveHandler,
12720                                 true
12721                         );
12722                 }
12723         }
12724         return false;
12728  * Handle mouse up events.
12730  * @private
12731  * @param {jQuery.Event} e Mouse up event
12732  */
12733 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
12734         var item;
12736         this.togglePressed( false );
12737         if ( !this.selecting ) {
12738                 item = this.getTargetItem( e );
12739                 if ( item && item.isSelectable() ) {
12740                         this.selecting = item;
12741                 }
12742         }
12743         if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
12744                 this.pressItem( null );
12745                 this.chooseItem( this.selecting );
12746                 this.selecting = null;
12747         }
12749         this.getElementDocument().removeEventListener(
12750                 'mouseup',
12751                 this.onMouseUpHandler,
12752                 true
12753         );
12754         this.getElementDocument().removeEventListener(
12755                 'mousemove',
12756                 this.onMouseMoveHandler,
12757                 true
12758         );
12760         return false;
12764  * Handle mouse move events.
12766  * @private
12767  * @param {jQuery.Event} e Mouse move event
12768  */
12769 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
12770         var item;
12772         if ( !this.isDisabled() && this.pressed ) {
12773                 item = this.getTargetItem( e );
12774                 if ( item && item !== this.selecting && item.isSelectable() ) {
12775                         this.pressItem( item );
12776                         this.selecting = item;
12777                 }
12778         }
12779         return false;
12783  * Handle mouse over events.
12785  * @private
12786  * @param {jQuery.Event} e Mouse over event
12787  */
12788 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
12789         var item;
12791         if ( !this.isDisabled() ) {
12792                 item = this.getTargetItem( e );
12793                 this.highlightItem( item && item.isHighlightable() ? item : null );
12794         }
12795         return false;
12799  * Handle mouse leave events.
12801  * @private
12802  * @param {jQuery.Event} e Mouse over event
12803  */
12804 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
12805         if ( !this.isDisabled() ) {
12806                 this.highlightItem( null );
12807         }
12808         return false;
12812  * Handle key down events.
12814  * @param {jQuery.Event} e Key down event
12815  */
12816 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
12817         var nextItem,
12818                 handled = false,
12819                 currentItem = this.getHighlightedItem() || this.getSelectedItem();
12821         if ( !this.isDisabled() && this.isVisible() ) {
12822                 switch ( e.keyCode ) {
12823                         case OO.ui.Keys.ENTER:
12824                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
12825                                         // Was only highlighted, now let's select it. No-op if already selected.
12826                                         this.chooseItem( currentItem );
12827                                         handled = true;
12828                                 }
12829                                 break;
12830                         case OO.ui.Keys.UP:
12831                         case OO.ui.Keys.LEFT:
12832                                 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
12833                                 handled = true;
12834                                 break;
12835                         case OO.ui.Keys.DOWN:
12836                         case OO.ui.Keys.RIGHT:
12837                                 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
12838                                 handled = true;
12839                                 break;
12840                         case OO.ui.Keys.ESCAPE:
12841                         case OO.ui.Keys.TAB:
12842                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
12843                                         currentItem.setHighlighted( false );
12844                                 }
12845                                 this.unbindKeyDownListener();
12846                                 // Don't prevent tabbing away / defocusing
12847                                 handled = false;
12848                                 break;
12849                 }
12851                 if ( nextItem ) {
12852                         if ( nextItem.constructor.static.highlightable ) {
12853                                 this.highlightItem( nextItem );
12854                         } else {
12855                                 this.chooseItem( nextItem );
12856                         }
12857                         nextItem.scrollElementIntoView();
12858                 }
12860                 if ( handled ) {
12861                         // Can't just return false, because e is not always a jQuery event
12862                         e.preventDefault();
12863                         e.stopPropagation();
12864                 }
12865         }
12869  * Bind key down listener.
12870  */
12871 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
12872         this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
12876  * Unbind key down listener.
12877  */
12878 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
12879         this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
12883  * Get the closest item to a jQuery.Event.
12885  * @private
12886  * @param {jQuery.Event} e
12887  * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
12888  */
12889 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
12890         var $item = $( e.target ).closest( '.oo-ui-optionWidget' );
12891         if ( $item.length ) {
12892                 return $item.data( 'oo-ui-optionWidget' );
12893         }
12894         return null;
12898  * Get selected item.
12900  * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
12901  */
12902 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
12903         var i, len;
12905         for ( i = 0, len = this.items.length; i < len; i++ ) {
12906                 if ( this.items[ i ].isSelected() ) {
12907                         return this.items[ i ];
12908                 }
12909         }
12910         return null;
12914  * Get highlighted item.
12916  * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
12917  */
12918 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
12919         var i, len;
12921         for ( i = 0, len = this.items.length; i < len; i++ ) {
12922                 if ( this.items[ i ].isHighlighted() ) {
12923                         return this.items[ i ];
12924                 }
12925         }
12926         return null;
12930  * Toggle pressed state.
12932  * @param {boolean} pressed An option is being pressed
12933  */
12934 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
12935         if ( pressed === undefined ) {
12936                 pressed = !this.pressed;
12937         }
12938         if ( pressed !== this.pressed ) {
12939                 this.$element
12940                         .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
12941                         .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
12942                 this.pressed = pressed;
12943         }
12947  * Highlight an item.
12949  * Highlighting is mutually exclusive.
12951  * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
12952  * @fires highlight
12953  * @chainable
12954  */
12955 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
12956         var i, len, highlighted,
12957                 changed = false;
12959         for ( i = 0, len = this.items.length; i < len; i++ ) {
12960                 highlighted = this.items[ i ] === item;
12961                 if ( this.items[ i ].isHighlighted() !== highlighted ) {
12962                         this.items[ i ].setHighlighted( highlighted );
12963                         changed = true;
12964                 }
12965         }
12966         if ( changed ) {
12967                 this.emit( 'highlight', item );
12968         }
12970         return this;
12974  * Select an item.
12976  * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
12977  * @fires select
12978  * @chainable
12979  */
12980 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
12981         var i, len, selected,
12982                 changed = false;
12984         for ( i = 0, len = this.items.length; i < len; i++ ) {
12985                 selected = this.items[ i ] === item;
12986                 if ( this.items[ i ].isSelected() !== selected ) {
12987                         this.items[ i ].setSelected( selected );
12988                         changed = true;
12989                 }
12990         }
12991         if ( changed ) {
12992                 this.emit( 'select', item );
12993         }
12995         return this;
12999  * Press an item.
13001  * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
13002  * @fires press
13003  * @chainable
13004  */
13005 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
13006         var i, len, pressed,
13007                 changed = false;
13009         for ( i = 0, len = this.items.length; i < len; i++ ) {
13010                 pressed = this.items[ i ] === item;
13011                 if ( this.items[ i ].isPressed() !== pressed ) {
13012                         this.items[ i ].setPressed( pressed );
13013                         changed = true;
13014                 }
13015         }
13016         if ( changed ) {
13017                 this.emit( 'press', item );
13018         }
13020         return this;
13024  * Choose an item.
13026  * Identical to #selectItem, but may vary in subclasses that want to take additional action when
13027  * an item is selected using the keyboard or mouse.
13029  * @param {OO.ui.OptionWidget} item Item to choose
13030  * @fires choose
13031  * @chainable
13032  */
13033 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
13034         this.selectItem( item );
13035         this.emit( 'choose', item );
13037         return this;
13041  * Get an item relative to another one.
13043  * @param {OO.ui.OptionWidget|null} item Item to start at, null to get relative to list start
13044  * @param {number} direction Direction to move in, -1 to move backward, 1 to move forward
13045  * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
13046  */
13047 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
13048         var currentIndex, nextIndex, i,
13049                 increase = direction > 0 ? 1 : -1,
13050                 len = this.items.length;
13052         if ( item instanceof OO.ui.OptionWidget ) {
13053                 currentIndex = $.inArray( item, this.items );
13054                 nextIndex = ( currentIndex + increase + len ) % len;
13055         } else {
13056                 // If no item is selected and moving forward, start at the beginning.
13057                 // If moving backward, start at the end.
13058                 nextIndex = direction > 0 ? 0 : len - 1;
13059         }
13061         for ( i = 0; i < len; i++ ) {
13062                 item = this.items[ nextIndex ];
13063                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
13064                         return item;
13065                 }
13066                 nextIndex = ( nextIndex + increase + len ) % len;
13067         }
13068         return null;
13072  * Get the next selectable item.
13074  * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
13075  */
13076 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
13077         var i, len, item;
13079         for ( i = 0, len = this.items.length; i < len; i++ ) {
13080                 item = this.items[ i ];
13081                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
13082                         return item;
13083                 }
13084         }
13086         return null;
13090  * Add items.
13092  * @param {OO.ui.OptionWidget[]} items Items to add
13093  * @param {number} [index] Index to insert items after
13094  * @fires add
13095  * @chainable
13096  */
13097 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
13098         // Mixin method
13099         OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
13101         // Always provide an index, even if it was omitted
13102         this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
13104         return this;
13108  * Remove items.
13110  * Items will be detached, not removed, so they can be used later.
13112  * @param {OO.ui.OptionWidget[]} items Items to remove
13113  * @fires remove
13114  * @chainable
13115  */
13116 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
13117         var i, len, item;
13119         // Deselect items being removed
13120         for ( i = 0, len = items.length; i < len; i++ ) {
13121                 item = items[ i ];
13122                 if ( item.isSelected() ) {
13123                         this.selectItem( null );
13124                 }
13125         }
13127         // Mixin method
13128         OO.ui.GroupWidget.prototype.removeItems.call( this, items );
13130         this.emit( 'remove', items );
13132         return this;
13136  * Clear all items.
13138  * Items will be detached, not removed, so they can be used later.
13140  * @fires remove
13141  * @chainable
13142  */
13143 OO.ui.SelectWidget.prototype.clearItems = function () {
13144         var items = this.items.slice();
13146         // Mixin method
13147         OO.ui.GroupWidget.prototype.clearItems.call( this );
13149         // Clear selection
13150         this.selectItem( null );
13152         this.emit( 'remove', items );
13154         return this;
13158  * Select widget containing button options.
13160  * Use together with OO.ui.ButtonOptionWidget.
13162  * @class
13163  * @extends OO.ui.SelectWidget
13164  * @mixins OO.ui.TabIndexedElement
13166  * @constructor
13167  * @param {Object} [config] Configuration options
13168  */
13169 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
13170         // Parent constructor
13171         OO.ui.ButtonSelectWidget.super.call( this, config );
13173         // Mixin constructors
13174         OO.ui.TabIndexedElement.call( this, config );
13176         // Events
13177         this.$element.on( {
13178                 focus: this.bindKeyDownListener.bind( this ),
13179                 blur: this.unbindKeyDownListener.bind( this )
13180         } );
13182         // Initialization
13183         this.$element.addClass( 'oo-ui-buttonSelectWidget' );
13186 /* Setup */
13188 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
13189 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.TabIndexedElement );
13192  * Select widget containing radio button options.
13194  * Use together with OO.ui.RadioOptionWidget.
13196  * @class
13197  * @extends OO.ui.SelectWidget
13198  * @mixins OO.ui.TabIndexedElement
13200  * @constructor
13201  * @param {Object} [config] Configuration options
13202  */
13203 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
13204         // Parent constructor
13205         OO.ui.RadioSelectWidget.super.call( this, config );
13207         // Mixin constructors
13208         OO.ui.TabIndexedElement.call( this, config );
13210         // Events
13211         this.$element.on( {
13212                 focus: this.bindKeyDownListener.bind( this ),
13213                 blur: this.unbindKeyDownListener.bind( this )
13214         } );
13216         // Initialization
13217         this.$element.addClass( 'oo-ui-radioSelectWidget' );
13220 /* Setup */
13222 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
13223 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.TabIndexedElement );
13226  * Overlaid menu of options.
13228  * Menus are clipped to the visible viewport. They do not provide a control for opening or closing
13229  * the menu.
13231  * Use together with OO.ui.MenuOptionWidget.
13233  * @class
13234  * @extends OO.ui.SelectWidget
13235  * @mixins OO.ui.ClippableElement
13237  * @constructor
13238  * @param {Object} [config] Configuration options
13239  * @cfg {OO.ui.TextInputWidget} [input] Input to bind keyboard handlers to
13240  * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
13241  * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
13242  */
13243 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
13244         // Configuration initialization
13245         config = config || {};
13247         // Parent constructor
13248         OO.ui.MenuSelectWidget.super.call( this, config );
13250         // Mixin constructors
13251         OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
13253         // Properties
13254         this.newItems = null;
13255         this.autoHide = config.autoHide === undefined || !!config.autoHide;
13256         this.$input = config.input ? config.input.$input : null;
13257         this.$widget = config.widget ? config.widget.$element : null;
13258         this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
13260         // Initialization
13261         this.$element
13262                 .addClass( 'oo-ui-menuSelectWidget' )
13263                 .attr( 'role', 'menu' );
13265         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
13266         // that reference properties not initialized at that time of parent class construction
13267         // TODO: Find a better way to handle post-constructor setup
13268         this.visible = false;
13269         this.$element.addClass( 'oo-ui-element-hidden' );
13272 /* Setup */
13274 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
13275 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement );
13277 /* Methods */
13280  * Handles document mouse down events.
13282  * @param {jQuery.Event} e Key down event
13283  */
13284 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
13285         if (
13286                 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
13287                 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
13288         ) {
13289                 this.toggle( false );
13290         }
13294  * @inheritdoc
13295  */
13296 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
13297         var currentItem = this.getHighlightedItem() || this.getSelectedItem();
13299         if ( !this.isDisabled() && this.isVisible() ) {
13300                 switch ( e.keyCode ) {
13301                         case OO.ui.Keys.LEFT:
13302                         case OO.ui.Keys.RIGHT:
13303                                 // Do nothing if a text field is associated, arrow keys will be handled natively
13304                                 if ( !this.$input ) {
13305                                         OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
13306                                 }
13307                                 break;
13308                         case OO.ui.Keys.ESCAPE:
13309                         case OO.ui.Keys.TAB:
13310                                 if ( currentItem ) {
13311                                         currentItem.setHighlighted( false );
13312                                 }
13313                                 this.toggle( false );
13314                                 // Don't prevent tabbing away, prevent defocusing
13315                                 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
13316                                         e.preventDefault();
13317                                         e.stopPropagation();
13318                                 }
13319                                 break;
13320                         default:
13321                                 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
13322                                 return;
13323                 }
13324         }
13328  * @inheritdoc
13329  */
13330 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
13331         if ( this.$input ) {
13332                 this.$input.on( 'keydown', this.onKeyDownHandler );
13333         } else {
13334                 OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this );
13335         }
13339  * @inheritdoc
13340  */
13341 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
13342         if ( this.$input ) {
13343                 this.$input.off( 'keydown', this.onKeyDownHandler );
13344         } else {
13345                 OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this );
13346         }
13350  * Choose an item.
13352  * This will close the menu, unlike #selectItem which only changes selection.
13354  * @param {OO.ui.OptionWidget} item Item to choose
13355  * @chainable
13356  */
13357 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
13358         OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
13359         this.toggle( false );
13360         return this;
13364  * @inheritdoc
13365  */
13366 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
13367         var i, len, item;
13369         // Parent method
13370         OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
13372         // Auto-initialize
13373         if ( !this.newItems ) {
13374                 this.newItems = [];
13375         }
13377         for ( i = 0, len = items.length; i < len; i++ ) {
13378                 item = items[ i ];
13379                 if ( this.isVisible() ) {
13380                         // Defer fitting label until item has been attached
13381                         item.fitLabel();
13382                 } else {
13383                         this.newItems.push( item );
13384                 }
13385         }
13387         // Reevaluate clipping
13388         this.clip();
13390         return this;
13394  * @inheritdoc
13395  */
13396 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
13397         // Parent method
13398         OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
13400         // Reevaluate clipping
13401         this.clip();
13403         return this;
13407  * @inheritdoc
13408  */
13409 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
13410         // Parent method
13411         OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
13413         // Reevaluate clipping
13414         this.clip();
13416         return this;
13420  * @inheritdoc
13421  */
13422 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
13423         visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
13425         var i, len,
13426                 change = visible !== this.isVisible();
13428         // Parent method
13429         OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
13431         if ( change ) {
13432                 if ( visible ) {
13433                         this.bindKeyDownListener();
13435                         if ( this.newItems && this.newItems.length ) {
13436                                 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
13437                                         this.newItems[ i ].fitLabel();
13438                                 }
13439                                 this.newItems = null;
13440                         }
13441                         this.toggleClipping( true );
13443                         // Auto-hide
13444                         if ( this.autoHide ) {
13445                                 this.getElementDocument().addEventListener(
13446                                         'mousedown', this.onDocumentMouseDownHandler, true
13447                                 );
13448                         }
13449                 } else {
13450                         this.unbindKeyDownListener();
13451                         this.getElementDocument().removeEventListener(
13452                                 'mousedown', this.onDocumentMouseDownHandler, true
13453                         );
13454                         this.toggleClipping( false );
13455                 }
13456         }
13458         return this;
13462  * Menu for a text input widget.
13464  * This menu is specially designed to be positioned beneath a text input widget. The menu's position
13465  * is automatically calculated and maintained when the menu is toggled or the window is resized.
13467  * @class
13468  * @extends OO.ui.MenuSelectWidget
13470  * @constructor
13471  * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
13472  * @param {Object} [config] Configuration options
13473  * @cfg {jQuery} [$container=input.$element] Element to render menu under
13474  */
13475 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( input, config ) {
13476         // Configuration initialization
13477         config = config || {};
13479         // Parent constructor
13480         OO.ui.TextInputMenuSelectWidget.super.call( this, config );
13482         // Properties
13483         this.input = input;
13484         this.$container = config.$container || this.input.$element;
13485         this.onWindowResizeHandler = this.onWindowResize.bind( this );
13487         // Initialization
13488         this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
13491 /* Setup */
13493 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
13495 /* Methods */
13498  * Handle window resize event.
13500  * @param {jQuery.Event} e Window resize event
13501  */
13502 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
13503         this.position();
13507  * @inheritdoc
13508  */
13509 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
13510         visible = visible === undefined ? !this.isVisible() : !!visible;
13512         var change = visible !== this.isVisible();
13514         if ( change && visible ) {
13515                 // Make sure the width is set before the parent method runs.
13516                 // After this we have to call this.position(); again to actually
13517                 // position ourselves correctly.
13518                 this.position();
13519         }
13521         // Parent method
13522         OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
13524         if ( change ) {
13525                 if ( this.isVisible() ) {
13526                         this.position();
13527                         $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
13528                 } else {
13529                         $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
13530                 }
13531         }
13533         return this;
13537  * Position the menu.
13539  * @chainable
13540  */
13541 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
13542         var $container = this.$container,
13543                 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
13545         // Position under input
13546         pos.top += $container.height();
13547         this.$element.css( pos );
13549         // Set width
13550         this.setIdealSize( $container.width() );
13551         // We updated the position, so re-evaluate the clipping state
13552         this.clip();
13554         return this;
13558  * Structured list of items.
13560  * Use with OO.ui.OutlineOptionWidget.
13562  * @class
13563  * @extends OO.ui.SelectWidget
13564  * @mixins OO.ui.TabIndexedElement
13566  * @constructor
13567  * @param {Object} [config] Configuration options
13568  */
13569 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
13570         // Parent constructor
13571         OO.ui.OutlineSelectWidget.super.call( this, config );
13573         // Mixin constructors
13574         OO.ui.TabIndexedElement.call( this, config );
13576         // Events
13577         this.$element.on( {
13578                 focus: this.bindKeyDownListener.bind( this ),
13579                 blur: this.unbindKeyDownListener.bind( this )
13580         } );
13582         // Initialization
13583         this.$element.addClass( 'oo-ui-outlineSelectWidget' );
13586 /* Setup */
13588 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
13589 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.TabIndexedElement );
13592  * Switch that slides on and off.
13594  * @class
13595  * @extends OO.ui.Widget
13596  * @mixins OO.ui.ToggleWidget
13597  * @mixins OO.ui.TabIndexedElement
13599  * @constructor
13600  * @param {Object} [config] Configuration options
13601  * @cfg {boolean} [value=false] Initial value
13602  */
13603 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
13604         // Parent constructor
13605         OO.ui.ToggleSwitchWidget.super.call( this, config );
13607         // Mixin constructors
13608         OO.ui.ToggleWidget.call( this, config );
13609         OO.ui.TabIndexedElement.call( this, config );
13611         // Properties
13612         this.dragging = false;
13613         this.dragStart = null;
13614         this.sliding = false;
13615         this.$glow = $( '<span>' );
13616         this.$grip = $( '<span>' );
13618         // Events
13619         this.$element.on( {
13620                 click: this.onClick.bind( this ),
13621                 keypress: this.onKeyPress.bind( this )
13622         } );
13624         // Initialization
13625         this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
13626         this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
13627         this.$element
13628                 .addClass( 'oo-ui-toggleSwitchWidget' )
13629                 .attr( 'role', 'checkbox' )
13630                 .append( this.$glow, this.$grip );
13633 /* Setup */
13635 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
13636 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
13637 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.TabIndexedElement );
13639 /* Methods */
13642  * Handle mouse click events.
13644  * @param {jQuery.Event} e Mouse click event
13645  */
13646 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
13647         if ( !this.isDisabled() && e.which === 1 ) {
13648                 this.setValue( !this.value );
13649         }
13650         return false;
13654  * Handle key press events.
13656  * @param {jQuery.Event} e Key press event
13657  */
13658 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
13659         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
13660                 this.setValue( !this.value );
13661         }
13662         return false;
13665 }( OO ) );