OOjs UI: Backport aadaa8a1, 9aba218a
[mediawiki.git] / resources / lib / oojs-ui / oojs-ui.js
blob951acc7f9c692600f850416921d83bea6100fc8d
1 /*
2  * Local backports:
3  *
4  * - 9aba218a882ff45b07410a3ce9d5cdfd8e567e26
5  *   CapsuleMultiSelectWidget: When 'allowArbitrary' is true, don't require 'Enter' to confirm
6  *   Required for more intuitive behavior of mw.widgets.CategorySelector.
7  */
9 /*!
10  * OOjs UI v0.12.11
11  * https://www.mediawiki.org/wiki/OOjs_UI
12  *
13  * Copyright 2011–2015 OOjs UI Team and other contributors.
14  * Released under the MIT license
15  * http://oojs.mit-license.org
16  *
17  * Date: 2015-10-07T20:48:15Z
18  */
19 ( function ( OO ) {
21 'use strict';
23 /**
24  * Namespace for all classes, static methods and static properties.
25  *
26  * @class
27  * @singleton
28  */
29 OO.ui = {};
31 OO.ui.bind = $.proxy;
33 /**
34  * @property {Object}
35  */
36 OO.ui.Keys = {
37         UNDEFINED: 0,
38         BACKSPACE: 8,
39         DELETE: 46,
40         LEFT: 37,
41         RIGHT: 39,
42         UP: 38,
43         DOWN: 40,
44         ENTER: 13,
45         END: 35,
46         HOME: 36,
47         TAB: 9,
48         PAGEUP: 33,
49         PAGEDOWN: 34,
50         ESCAPE: 27,
51         SHIFT: 16,
52         SPACE: 32
55 /**
56  * @property {Number}
57  */
58 OO.ui.elementId = 0;
60 /**
61  * Generate a unique ID for element
62  *
63  * @return {String} [id]
64  */
65 OO.ui.generateElementId = function () {
66         OO.ui.elementId += 1;
67         return 'oojsui-' + OO.ui.elementId;
70 /**
71  * Check if an element is focusable.
72  * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
73  *
74  * @param {jQuery} element Element to test
75  * @return {boolean}
76  */
77 OO.ui.isFocusableElement = function ( $element ) {
78         var nodeName,
79                 element = $element[ 0 ];
81         // Anything disabled is not focusable
82         if ( element.disabled ) {
83                 return false;
84         }
86         // Check if the element is visible
87         if ( !(
88                 // This is quicker than calling $element.is( ':visible' )
89                 $.expr.filters.visible( element ) &&
90                 // Check that all parents are visible
91                 !$element.parents().addBack().filter( function () {
92                         return $.css( this, 'visibility' ) === 'hidden';
93                 } ).length
94         ) ) {
95                 return false;
96         }
98         // Check if the element is ContentEditable, which is the string 'true'
99         if ( element.contentEditable === 'true' ) {
100                 return true;
101         }
103         // Anything with a non-negative numeric tabIndex is focusable.
104         // Use .prop to avoid browser bugs
105         if ( $element.prop( 'tabIndex' ) >= 0 ) {
106                 return true;
107         }
109         // Some element types are naturally focusable
110         // (indexOf is much faster than regex in Chrome and about the
111         // same in FF: https://jsperf.com/regex-vs-indexof-array2)
112         nodeName = element.nodeName.toLowerCase();
113         if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName ) !== -1 ) {
114                 return true;
115         }
117         // Links and areas are focusable if they have an href
118         if ( ( nodeName === 'a' || nodeName === 'area' ) && $element.attr( 'href' ) !== undefined ) {
119                 return true;
120         }
122         return false;
126  * Find a focusable child
128  * @param {jQuery} $container Container to search in
129  * @param {boolean} [backwards] Search backwards
130  * @return {jQuery} Focusable child, an empty jQuery object if none found
131  */
132 OO.ui.findFocusable = function ( $container, backwards ) {
133         var $focusable = $( [] ),
134                 // $focusableCandidates is a superset of things that
135                 // could get matched by isFocusableElement
136                 $focusableCandidates = $container
137                         .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
139         if ( backwards ) {
140                 $focusableCandidates = Array.prototype.reverse.call( $focusableCandidates );
141         }
143         $focusableCandidates.each( function () {
144                 var $this = $( this );
145                 if ( OO.ui.isFocusableElement( $this ) ) {
146                         $focusable = $this;
147                         return false;
148                 }
149         } );
150         return $focusable;
154  * Get the user's language and any fallback languages.
156  * These language codes are used to localize user interface elements in the user's language.
158  * In environments that provide a localization system, this function should be overridden to
159  * return the user's language(s). The default implementation returns English (en) only.
161  * @return {string[]} Language codes, in descending order of priority
162  */
163 OO.ui.getUserLanguages = function () {
164         return [ 'en' ];
168  * Get a value in an object keyed by language code.
170  * @param {Object.<string,Mixed>} obj Object keyed by language code
171  * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
172  * @param {string} [fallback] Fallback code, used if no matching language can be found
173  * @return {Mixed} Local value
174  */
175 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
176         var i, len, langs;
178         // Requested language
179         if ( obj[ lang ] ) {
180                 return obj[ lang ];
181         }
182         // Known user language
183         langs = OO.ui.getUserLanguages();
184         for ( i = 0, len = langs.length; i < len; i++ ) {
185                 lang = langs[ i ];
186                 if ( obj[ lang ] ) {
187                         return obj[ lang ];
188                 }
189         }
190         // Fallback language
191         if ( obj[ fallback ] ) {
192                 return obj[ fallback ];
193         }
194         // First existing language
195         for ( lang in obj ) {
196                 return obj[ lang ];
197         }
199         return undefined;
203  * Check if a node is contained within another node
205  * Similar to jQuery#contains except a list of containers can be supplied
206  * and a boolean argument allows you to include the container in the match list
208  * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
209  * @param {HTMLElement} contained Node to find
210  * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
211  * @return {boolean} The node is in the list of target nodes
212  */
213 OO.ui.contains = function ( containers, contained, matchContainers ) {
214         var i;
215         if ( !Array.isArray( containers ) ) {
216                 containers = [ containers ];
217         }
218         for ( i = containers.length - 1; i >= 0; i-- ) {
219                 if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
220                         return true;
221                 }
222         }
223         return false;
227  * Return a function, that, as long as it continues to be invoked, will not
228  * be triggered. The function will be called after it stops being called for
229  * N milliseconds. If `immediate` is passed, trigger the function on the
230  * leading edge, instead of the trailing.
232  * Ported from: http://underscorejs.org/underscore.js
234  * @param {Function} func
235  * @param {number} wait
236  * @param {boolean} immediate
237  * @return {Function}
238  */
239 OO.ui.debounce = function ( func, wait, immediate ) {
240         var timeout;
241         return function () {
242                 var context = this,
243                         args = arguments,
244                         later = function () {
245                                 timeout = null;
246                                 if ( !immediate ) {
247                                         func.apply( context, args );
248                                 }
249                         };
250                 if ( immediate && !timeout ) {
251                         func.apply( context, args );
252                 }
253                 clearTimeout( timeout );
254                 timeout = setTimeout( later, wait );
255         };
259  * Proxy for `node.addEventListener( eventName, handler, true )`, if the browser supports it.
260  * Otherwise falls back to non-capturing event listeners.
262  * @param {HTMLElement} node
263  * @param {string} eventName
264  * @param {Function} handler
265  */
266 OO.ui.addCaptureEventListener = function ( node, eventName, handler ) {
267         if ( node.addEventListener ) {
268                 node.addEventListener( eventName, handler, true );
269         } else {
270                 node.attachEvent( 'on' + eventName, handler );
271         }
275  * Proxy for `node.removeEventListener( eventName, handler, true )`, if the browser supports it.
276  * Otherwise falls back to non-capturing event listeners.
278  * @param {HTMLElement} node
279  * @param {string} eventName
280  * @param {Function} handler
281  */
282 OO.ui.removeCaptureEventListener = function ( node, eventName, handler ) {
283         if ( node.addEventListener ) {
284                 node.removeEventListener( eventName, handler, true );
285         } else {
286                 node.detachEvent( 'on' + eventName, handler );
287         }
291  * Reconstitute a JavaScript object corresponding to a widget created by
292  * the PHP implementation.
294  * This is an alias for `OO.ui.Element.static.infuse()`.
296  * @param {string|HTMLElement|jQuery} idOrNode
297  *   A DOM id (if a string) or node for the widget to infuse.
298  * @return {OO.ui.Element}
299  *   The `OO.ui.Element` corresponding to this (infusable) document node.
300  */
301 OO.ui.infuse = function ( idOrNode ) {
302         return OO.ui.Element.static.infuse( idOrNode );
305 ( function () {
306         /**
307          * Message store for the default implementation of OO.ui.msg
308          *
309          * Environments that provide a localization system should not use this, but should override
310          * OO.ui.msg altogether.
311          *
312          * @private
313          */
314         var messages = {
315                 // Tool tip for a button that moves items in a list down one place
316                 'ooui-outline-control-move-down': 'Move item down',
317                 // Tool tip for a button that moves items in a list up one place
318                 'ooui-outline-control-move-up': 'Move item up',
319                 // Tool tip for a button that removes items from a list
320                 'ooui-outline-control-remove': 'Remove item',
321                 // Label for the toolbar group that contains a list of all other available tools
322                 'ooui-toolbar-more': 'More',
323                 // Label for the fake tool that expands the full list of tools in a toolbar group
324                 'ooui-toolgroup-expand': 'More',
325                 // Label for the fake tool that collapses the full list of tools in a toolbar group
326                 'ooui-toolgroup-collapse': 'Fewer',
327                 // Default label for the accept button of a confirmation dialog
328                 'ooui-dialog-message-accept': 'OK',
329                 // Default label for the reject button of a confirmation dialog
330                 'ooui-dialog-message-reject': 'Cancel',
331                 // Title for process dialog error description
332                 'ooui-dialog-process-error': 'Something went wrong',
333                 // Label for process dialog dismiss error button, visible when describing errors
334                 'ooui-dialog-process-dismiss': 'Dismiss',
335                 // Label for process dialog retry action button, visible when describing only recoverable errors
336                 'ooui-dialog-process-retry': 'Try again',
337                 // Label for process dialog retry action button, visible when describing only warnings
338                 'ooui-dialog-process-continue': 'Continue',
339                 // Label for the file selection widget's select file button
340                 'ooui-selectfile-button-select': 'Select a file',
341                 // Label for the file selection widget if file selection is not supported
342                 'ooui-selectfile-not-supported': 'File selection is not supported',
343                 // Label for the file selection widget when no file is currently selected
344                 'ooui-selectfile-placeholder': 'No file is selected',
345                 // Label for the file selection widget's drop target
346                 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
347         };
349         /**
350          * Get a localized message.
351          *
352          * In environments that provide a localization system, this function should be overridden to
353          * return the message translated in the user's language. The default implementation always returns
354          * English messages.
355          *
356          * After the message key, message parameters may optionally be passed. In the default implementation,
357          * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
358          * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
359          * they support unnamed, ordered message parameters.
360          *
361          * @abstract
362          * @param {string} key Message key
363          * @param {Mixed...} [params] Message parameters
364          * @return {string} Translated message with parameters substituted
365          */
366         OO.ui.msg = function ( key ) {
367                 var message = messages[ key ],
368                         params = Array.prototype.slice.call( arguments, 1 );
369                 if ( typeof message === 'string' ) {
370                         // Perform $1 substitution
371                         message = message.replace( /\$(\d+)/g, function ( unused, n ) {
372                                 var i = parseInt( n, 10 );
373                                 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
374                         } );
375                 } else {
376                         // Return placeholder if message not found
377                         message = '[' + key + ']';
378                 }
379                 return message;
380         };
382         /**
383          * Package a message and arguments for deferred resolution.
384          *
385          * Use this when you are statically specifying a message and the message may not yet be present.
386          *
387          * @param {string} key Message key
388          * @param {Mixed...} [params] Message parameters
389          * @return {Function} Function that returns the resolved message when executed
390          */
391         OO.ui.deferMsg = function () {
392                 var args = arguments;
393                 return function () {
394                         return OO.ui.msg.apply( OO.ui, args );
395                 };
396         };
398         /**
399          * Resolve a message.
400          *
401          * If the message is a function it will be executed, otherwise it will pass through directly.
402          *
403          * @param {Function|string} msg Deferred message, or message text
404          * @return {string} Resolved message
405          */
406         OO.ui.resolveMsg = function ( msg ) {
407                 if ( $.isFunction( msg ) ) {
408                         return msg();
409                 }
410                 return msg;
411         };
413         /**
414          * @param {string} url
415          * @return {boolean}
416          */
417         OO.ui.isSafeUrl = function ( url ) {
418                 var protocol,
419                         // Keep in sync with php/Tag.php
420                         whitelist = [
421                                 'bitcoin:', 'ftp:', 'ftps:', 'geo:', 'git:', 'gopher:', 'http:', 'https:', 'irc:', 'ircs:',
422                                 'magnet:', 'mailto:', 'mms:', 'news:', 'nntp:', 'redis:', 'sftp:', 'sip:', 'sips:', 'sms:', 'ssh:',
423                                 'svn:', 'tel:', 'telnet:', 'urn:', 'worldwind:', 'xmpp:'
424                         ];
426                 if ( url.indexOf( ':' ) === -1 ) {
427                         // No protocol, safe
428                         return true;
429                 }
431                 protocol = url.split( ':', 1 )[ 0 ] + ':';
432                 if ( !protocol.match( /^([A-za-z0-9\+\.\-])+:/ ) ) {
433                         // Not a valid protocol, safe
434                         return true;
435                 }
437                 // Safe if in the whitelist
438                 return whitelist.indexOf( protocol ) !== -1;
439         };
441 } )();
444  * Mixin namespace.
445  */
448  * Namespace for OOjs UI mixins.
450  * Mixins are named according to the type of object they are intended to
451  * be mixed in to.  For example, OO.ui.mixin.GroupElement is intended to be
452  * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
453  * is intended to be mixed in to an instance of OO.ui.Widget.
455  * @class
456  * @singleton
457  */
458 OO.ui.mixin = {};
461  * PendingElement is a mixin that is used to create elements that notify users that something is happening
462  * and that they should wait before proceeding. The pending state is visually represented with a pending
463  * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
464  * field of a {@link OO.ui.TextInputWidget text input widget}.
466  * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
467  * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
468  * in process dialogs.
470  *     @example
471  *     function MessageDialog( config ) {
472  *         MessageDialog.parent.call( this, config );
473  *     }
474  *     OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
476  *     MessageDialog.static.actions = [
477  *         { action: 'save', label: 'Done', flags: 'primary' },
478  *         { label: 'Cancel', flags: 'safe' }
479  *     ];
481  *     MessageDialog.prototype.initialize = function () {
482  *         MessageDialog.parent.prototype.initialize.apply( this, arguments );
483  *         this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
484  *         this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.</p>' );
485  *         this.$body.append( this.content.$element );
486  *     };
487  *     MessageDialog.prototype.getBodyHeight = function () {
488  *         return 100;
489  *     }
490  *     MessageDialog.prototype.getActionProcess = function ( action ) {
491  *         var dialog = this;
492  *         if ( action === 'save' ) {
493  *             dialog.getActions().get({actions: 'save'})[0].pushPending();
494  *             return new OO.ui.Process()
495  *             .next( 1000 )
496  *             .next( function () {
497  *                 dialog.getActions().get({actions: 'save'})[0].popPending();
498  *             } );
499  *         }
500  *         return MessageDialog.parent.prototype.getActionProcess.call( this, action );
501  *     };
503  *     var windowManager = new OO.ui.WindowManager();
504  *     $( 'body' ).append( windowManager.$element );
506  *     var dialog = new MessageDialog();
507  *     windowManager.addWindows( [ dialog ] );
508  *     windowManager.openWindow( dialog );
510  * @abstract
511  * @class
513  * @constructor
514  * @param {Object} [config] Configuration options
515  * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
516  */
517 OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
518         // Configuration initialization
519         config = config || {};
521         // Properties
522         this.pending = 0;
523         this.$pending = null;
525         // Initialisation
526         this.setPendingElement( config.$pending || this.$element );
529 /* Setup */
531 OO.initClass( OO.ui.mixin.PendingElement );
533 /* Methods */
536  * Set the pending element (and clean up any existing one).
538  * @param {jQuery} $pending The element to set to pending.
539  */
540 OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
541         if ( this.$pending ) {
542                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
543         }
545         this.$pending = $pending;
546         if ( this.pending > 0 ) {
547                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
548         }
552  * Check if an element is pending.
554  * @return {boolean} Element is pending
555  */
556 OO.ui.mixin.PendingElement.prototype.isPending = function () {
557         return !!this.pending;
561  * Increase the pending counter. The pending state will remain active until the counter is zero
562  * (i.e., the number of calls to #pushPending and #popPending is the same).
564  * @chainable
565  */
566 OO.ui.mixin.PendingElement.prototype.pushPending = function () {
567         if ( this.pending === 0 ) {
568                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
569                 this.updateThemeClasses();
570         }
571         this.pending++;
573         return this;
577  * Decrease the pending counter. The pending state will remain active until the counter is zero
578  * (i.e., the number of calls to #pushPending and #popPending is the same).
580  * @chainable
581  */
582 OO.ui.mixin.PendingElement.prototype.popPending = function () {
583         if ( this.pending === 1 ) {
584                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
585                 this.updateThemeClasses();
586         }
587         this.pending = Math.max( 0, this.pending - 1 );
589         return this;
593  * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
594  * Actions can be made available for specific contexts (modes) and circumstances
595  * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
597  * ActionSets contain two types of actions:
599  * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property.
600  * - Other: Other actions include all non-special visible actions.
602  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
604  *     @example
605  *     // Example: An action set used in a process dialog
606  *     function MyProcessDialog( config ) {
607  *         MyProcessDialog.parent.call( this, config );
608  *     }
609  *     OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
610  *     MyProcessDialog.static.title = 'An action set in a process dialog';
611  *     // An action set that uses modes ('edit' and 'help' mode, in this example).
612  *     MyProcessDialog.static.actions = [
613  *         { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
614  *         { action: 'help', modes: 'edit', label: 'Help' },
615  *         { modes: 'edit', label: 'Cancel', flags: 'safe' },
616  *         { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
617  *     ];
619  *     MyProcessDialog.prototype.initialize = function () {
620  *         MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
621  *         this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
622  *         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>' );
623  *         this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
624  *         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>' );
625  *         this.stackLayout = new OO.ui.StackLayout( {
626  *             items: [ this.panel1, this.panel2 ]
627  *         } );
628  *         this.$body.append( this.stackLayout.$element );
629  *     };
630  *     MyProcessDialog.prototype.getSetupProcess = function ( data ) {
631  *         return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
632  *             .next( function () {
633  *                 this.actions.setMode( 'edit' );
634  *             }, this );
635  *     };
636  *     MyProcessDialog.prototype.getActionProcess = function ( action ) {
637  *         if ( action === 'help' ) {
638  *             this.actions.setMode( 'help' );
639  *             this.stackLayout.setItem( this.panel2 );
640  *         } else if ( action === 'back' ) {
641  *             this.actions.setMode( 'edit' );
642  *             this.stackLayout.setItem( this.panel1 );
643  *         } else if ( action === 'continue' ) {
644  *             var dialog = this;
645  *             return new OO.ui.Process( function () {
646  *                 dialog.close();
647  *             } );
648  *         }
649  *         return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
650  *     };
651  *     MyProcessDialog.prototype.getBodyHeight = function () {
652  *         return this.panel1.$element.outerHeight( true );
653  *     };
654  *     var windowManager = new OO.ui.WindowManager();
655  *     $( 'body' ).append( windowManager.$element );
656  *     var dialog = new MyProcessDialog( {
657  *         size: 'medium'
658  *     } );
659  *     windowManager.addWindows( [ dialog ] );
660  *     windowManager.openWindow( dialog );
662  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
664  * @abstract
665  * @class
666  * @mixins OO.EventEmitter
668  * @constructor
669  * @param {Object} [config] Configuration options
670  */
671 OO.ui.ActionSet = function OoUiActionSet( config ) {
672         // Configuration initialization
673         config = config || {};
675         // Mixin constructors
676         OO.EventEmitter.call( this );
678         // Properties
679         this.list = [];
680         this.categories = {
681                 actions: 'getAction',
682                 flags: 'getFlags',
683                 modes: 'getModes'
684         };
685         this.categorized = {};
686         this.special = {};
687         this.others = [];
688         this.organized = false;
689         this.changing = false;
690         this.changed = false;
693 /* Setup */
695 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
697 /* Static Properties */
700  * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
701  *  header of a {@link OO.ui.ProcessDialog process dialog}.
702  *  See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
704  *  [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
706  * @abstract
707  * @static
708  * @inheritable
709  * @property {string}
710  */
711 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
713 /* Events */
716  * @event click
718  * A 'click' event is emitted when an action is clicked.
720  * @param {OO.ui.ActionWidget} action Action that was clicked
721  */
724  * @event resize
726  * A 'resize' event is emitted when an action widget is resized.
728  * @param {OO.ui.ActionWidget} action Action that was resized
729  */
732  * @event add
734  * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
736  * @param {OO.ui.ActionWidget[]} added Actions added
737  */
740  * @event remove
742  * A 'remove' event is emitted when actions are {@link #method-remove removed}
743  *  or {@link #clear cleared}.
745  * @param {OO.ui.ActionWidget[]} added Actions removed
746  */
749  * @event change
751  * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
752  * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
754  */
756 /* Methods */
759  * Handle action change events.
761  * @private
762  * @fires change
763  */
764 OO.ui.ActionSet.prototype.onActionChange = function () {
765         this.organized = false;
766         if ( this.changing ) {
767                 this.changed = true;
768         } else {
769                 this.emit( 'change' );
770         }
774  * Check if an action is one of the special actions.
776  * @param {OO.ui.ActionWidget} action Action to check
777  * @return {boolean} Action is special
778  */
779 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
780         var flag;
782         for ( flag in this.special ) {
783                 if ( action === this.special[ flag ] ) {
784                         return true;
785                 }
786         }
788         return false;
792  * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
793  *  or ‘disabled’.
795  * @param {Object} [filters] Filters to use, omit to get all actions
796  * @param {string|string[]} [filters.actions] Actions that action widgets must have
797  * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
798  * @param {string|string[]} [filters.modes] Modes that action widgets must have
799  * @param {boolean} [filters.visible] Action widgets must be visible
800  * @param {boolean} [filters.disabled] Action widgets must be disabled
801  * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
802  */
803 OO.ui.ActionSet.prototype.get = function ( filters ) {
804         var i, len, list, category, actions, index, match, matches;
806         if ( filters ) {
807                 this.organize();
809                 // Collect category candidates
810                 matches = [];
811                 for ( category in this.categorized ) {
812                         list = filters[ category ];
813                         if ( list ) {
814                                 if ( !Array.isArray( list ) ) {
815                                         list = [ list ];
816                                 }
817                                 for ( i = 0, len = list.length; i < len; i++ ) {
818                                         actions = this.categorized[ category ][ list[ i ] ];
819                                         if ( Array.isArray( actions ) ) {
820                                                 matches.push.apply( matches, actions );
821                                         }
822                                 }
823                         }
824                 }
825                 // Remove by boolean filters
826                 for ( i = 0, len = matches.length; i < len; i++ ) {
827                         match = matches[ i ];
828                         if (
829                                 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
830                                 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
831                         ) {
832                                 matches.splice( i, 1 );
833                                 len--;
834                                 i--;
835                         }
836                 }
837                 // Remove duplicates
838                 for ( i = 0, len = matches.length; i < len; i++ ) {
839                         match = matches[ i ];
840                         index = matches.lastIndexOf( match );
841                         while ( index !== i ) {
842                                 matches.splice( index, 1 );
843                                 len--;
844                                 index = matches.lastIndexOf( match );
845                         }
846                 }
847                 return matches;
848         }
849         return this.list.slice();
853  * Get 'special' actions.
855  * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
856  * Special flags can be configured in subclasses by changing the static #specialFlags property.
858  * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
859  */
860 OO.ui.ActionSet.prototype.getSpecial = function () {
861         this.organize();
862         return $.extend( {}, this.special );
866  * Get 'other' actions.
868  * Other actions include all non-special visible action widgets.
870  * @return {OO.ui.ActionWidget[]} 'Other' action widgets
871  */
872 OO.ui.ActionSet.prototype.getOthers = function () {
873         this.organize();
874         return this.others.slice();
878  * Set the mode  (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
879  * to be available in the specified mode will be made visible. All other actions will be hidden.
881  * @param {string} mode The mode. Only actions configured to be available in the specified
882  *  mode will be made visible.
883  * @chainable
884  * @fires toggle
885  * @fires change
886  */
887 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
888         var i, len, action;
890         this.changing = true;
891         for ( i = 0, len = this.list.length; i < len; i++ ) {
892                 action = this.list[ i ];
893                 action.toggle( action.hasMode( mode ) );
894         }
896         this.organized = false;
897         this.changing = false;
898         this.emit( 'change' );
900         return this;
904  * Set the abilities of the specified actions.
906  * Action widgets that are configured with the specified actions will be enabled
907  * or disabled based on the boolean values specified in the `actions`
908  * parameter.
910  * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
911  *  values that indicate whether or not the action should be enabled.
912  * @chainable
913  */
914 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
915         var i, len, action, item;
917         for ( i = 0, len = this.list.length; i < len; i++ ) {
918                 item = this.list[ i ];
919                 action = item.getAction();
920                 if ( actions[ action ] !== undefined ) {
921                         item.setDisabled( !actions[ action ] );
922                 }
923         }
925         return this;
929  * Executes a function once per action.
931  * When making changes to multiple actions, use this method instead of iterating over the actions
932  * manually to defer emitting a #change event until after all actions have been changed.
934  * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
935  * @param {Function} callback Callback to run for each action; callback is invoked with three
936  *   arguments: the action, the action's index, the list of actions being iterated over
937  * @chainable
938  */
939 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
940         this.changed = false;
941         this.changing = true;
942         this.get( filter ).forEach( callback );
943         this.changing = false;
944         if ( this.changed ) {
945                 this.emit( 'change' );
946         }
948         return this;
952  * Add action widgets to the action set.
954  * @param {OO.ui.ActionWidget[]} actions Action widgets to add
955  * @chainable
956  * @fires add
957  * @fires change
958  */
959 OO.ui.ActionSet.prototype.add = function ( actions ) {
960         var i, len, action;
962         this.changing = true;
963         for ( i = 0, len = actions.length; i < len; i++ ) {
964                 action = actions[ i ];
965                 action.connect( this, {
966                         click: [ 'emit', 'click', action ],
967                         resize: [ 'emit', 'resize', action ],
968                         toggle: [ 'onActionChange' ]
969                 } );
970                 this.list.push( action );
971         }
972         this.organized = false;
973         this.emit( 'add', actions );
974         this.changing = false;
975         this.emit( 'change' );
977         return this;
981  * Remove action widgets from the set.
983  * To remove all actions, you may wish to use the #clear method instead.
985  * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
986  * @chainable
987  * @fires remove
988  * @fires change
989  */
990 OO.ui.ActionSet.prototype.remove = function ( actions ) {
991         var i, len, index, action;
993         this.changing = true;
994         for ( i = 0, len = actions.length; i < len; i++ ) {
995                 action = actions[ i ];
996                 index = this.list.indexOf( action );
997                 if ( index !== -1 ) {
998                         action.disconnect( this );
999                         this.list.splice( index, 1 );
1000                 }
1001         }
1002         this.organized = false;
1003         this.emit( 'remove', actions );
1004         this.changing = false;
1005         this.emit( 'change' );
1007         return this;
1011  * Remove all action widets from the set.
1013  * To remove only specified actions, use the {@link #method-remove remove} method instead.
1015  * @chainable
1016  * @fires remove
1017  * @fires change
1018  */
1019 OO.ui.ActionSet.prototype.clear = function () {
1020         var i, len, action,
1021                 removed = this.list.slice();
1023         this.changing = true;
1024         for ( i = 0, len = this.list.length; i < len; i++ ) {
1025                 action = this.list[ i ];
1026                 action.disconnect( this );
1027         }
1029         this.list = [];
1031         this.organized = false;
1032         this.emit( 'remove', removed );
1033         this.changing = false;
1034         this.emit( 'change' );
1036         return this;
1040  * Organize actions.
1042  * This is called whenever organized information is requested. It will only reorganize the actions
1043  * if something has changed since the last time it ran.
1045  * @private
1046  * @chainable
1047  */
1048 OO.ui.ActionSet.prototype.organize = function () {
1049         var i, iLen, j, jLen, flag, action, category, list, item, special,
1050                 specialFlags = this.constructor.static.specialFlags;
1052         if ( !this.organized ) {
1053                 this.categorized = {};
1054                 this.special = {};
1055                 this.others = [];
1056                 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
1057                         action = this.list[ i ];
1058                         if ( action.isVisible() ) {
1059                                 // Populate categories
1060                                 for ( category in this.categories ) {
1061                                         if ( !this.categorized[ category ] ) {
1062                                                 this.categorized[ category ] = {};
1063                                         }
1064                                         list = action[ this.categories[ category ] ]();
1065                                         if ( !Array.isArray( list ) ) {
1066                                                 list = [ list ];
1067                                         }
1068                                         for ( j = 0, jLen = list.length; j < jLen; j++ ) {
1069                                                 item = list[ j ];
1070                                                 if ( !this.categorized[ category ][ item ] ) {
1071                                                         this.categorized[ category ][ item ] = [];
1072                                                 }
1073                                                 this.categorized[ category ][ item ].push( action );
1074                                         }
1075                                 }
1076                                 // Populate special/others
1077                                 special = false;
1078                                 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
1079                                         flag = specialFlags[ j ];
1080                                         if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
1081                                                 this.special[ flag ] = action;
1082                                                 special = true;
1083                                                 break;
1084                                         }
1085                                 }
1086                                 if ( !special ) {
1087                                         this.others.push( action );
1088                                 }
1089                         }
1090                 }
1091                 this.organized = true;
1092         }
1094         return this;
1098  * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
1099  * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
1100  * connected to them and can't be interacted with.
1102  * @abstract
1103  * @class
1105  * @constructor
1106  * @param {Object} [config] Configuration options
1107  * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
1108  *  to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
1109  *  for an example.
1110  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
1111  * @cfg {string} [id] The HTML id attribute used in the rendered tag.
1112  * @cfg {string} [text] Text to insert
1113  * @cfg {Array} [content] An array of content elements to append (after #text).
1114  *  Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
1115  *  Instances of OO.ui.Element will have their $element appended.
1116  * @cfg {jQuery} [$content] Content elements to append (after #text)
1117  * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
1118  *  Data can also be specified with the #setData method.
1119  */
1120 OO.ui.Element = function OoUiElement( config ) {
1121         // Configuration initialization
1122         config = config || {};
1124         // Properties
1125         this.$ = $;
1126         this.visible = true;
1127         this.data = config.data;
1128         this.$element = config.$element ||
1129                 $( document.createElement( this.getTagName() ) );
1130         this.elementGroup = null;
1131         this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses );
1133         // Initialization
1134         if ( Array.isArray( config.classes ) ) {
1135                 this.$element.addClass( config.classes.join( ' ' ) );
1136         }
1137         if ( config.id ) {
1138                 this.$element.attr( 'id', config.id );
1139         }
1140         if ( config.text ) {
1141                 this.$element.text( config.text );
1142         }
1143         if ( config.content ) {
1144                 // The `content` property treats plain strings as text; use an
1145                 // HtmlSnippet to append HTML content.  `OO.ui.Element`s get their
1146                 // appropriate $element appended.
1147                 this.$element.append( config.content.map( function ( v ) {
1148                         if ( typeof v === 'string' ) {
1149                                 // Escape string so it is properly represented in HTML.
1150                                 return document.createTextNode( v );
1151                         } else if ( v instanceof OO.ui.HtmlSnippet ) {
1152                                 // Bypass escaping.
1153                                 return v.toString();
1154                         } else if ( v instanceof OO.ui.Element ) {
1155                                 return v.$element;
1156                         }
1157                         return v;
1158                 } ) );
1159         }
1160         if ( config.$content ) {
1161                 // The `$content` property treats plain strings as HTML.
1162                 this.$element.append( config.$content );
1163         }
1166 /* Setup */
1168 OO.initClass( OO.ui.Element );
1170 /* Static Properties */
1173  * The name of the HTML tag used by the element.
1175  * The static value may be ignored if the #getTagName method is overridden.
1177  * @static
1178  * @inheritable
1179  * @property {string}
1180  */
1181 OO.ui.Element.static.tagName = 'div';
1183 /* Static Methods */
1186  * Reconstitute a JavaScript object corresponding to a widget created
1187  * by the PHP implementation.
1189  * @param {string|HTMLElement|jQuery} idOrNode
1190  *   A DOM id (if a string) or node for the widget to infuse.
1191  * @return {OO.ui.Element}
1192  *   The `OO.ui.Element` corresponding to this (infusable) document node.
1193  *   For `Tag` objects emitted on the HTML side (used occasionally for content)
1194  *   the value returned is a newly-created Element wrapping around the existing
1195  *   DOM node.
1196  */
1197 OO.ui.Element.static.infuse = function ( idOrNode ) {
1198         var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
1199         // Verify that the type matches up.
1200         // FIXME: uncomment after T89721 is fixed (see T90929)
1201         /*
1202         if ( !( obj instanceof this['class'] ) ) {
1203                 throw new Error( 'Infusion type mismatch!' );
1204         }
1205         */
1206         return obj;
1210  * Implementation helper for `infuse`; skips the type check and has an
1211  * extra property so that only the top-level invocation touches the DOM.
1212  * @private
1213  * @param {string|HTMLElement|jQuery} idOrNode
1214  * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
1215  *     when the top-level widget of this infusion is inserted into DOM,
1216  *     replacing the original node; or false for top-level invocation.
1217  * @return {OO.ui.Element}
1218  */
1219 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
1220         // look for a cached result of a previous infusion.
1221         var id, $elem, data, cls, parts, parent, obj, top, state;
1222         if ( typeof idOrNode === 'string' ) {
1223                 id = idOrNode;
1224                 $elem = $( document.getElementById( id ) );
1225         } else {
1226                 $elem = $( idOrNode );
1227                 id = $elem.attr( 'id' );
1228         }
1229         if ( !$elem.length ) {
1230                 throw new Error( 'Widget not found: ' + id );
1231         }
1232         data = $elem.data( 'ooui-infused' ) || $elem[ 0 ].oouiInfused;
1233         if ( data ) {
1234                 // cached!
1235                 if ( data === true ) {
1236                         throw new Error( 'Circular dependency! ' + id );
1237                 }
1238                 return data;
1239         }
1240         data = $elem.attr( 'data-ooui' );
1241         if ( !data ) {
1242                 throw new Error( 'No infusion data found: ' + id );
1243         }
1244         try {
1245                 data = $.parseJSON( data );
1246         } catch ( _ ) {
1247                 data = null;
1248         }
1249         if ( !( data && data._ ) ) {
1250                 throw new Error( 'No valid infusion data found: ' + id );
1251         }
1252         if ( data._ === 'Tag' ) {
1253                 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
1254                 return new OO.ui.Element( { $element: $elem } );
1255         }
1256         parts = data._.split( '.' );
1257         cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
1258         if ( cls === undefined ) {
1259                 // The PHP output might be old and not including the "OO.ui" prefix
1260                 // TODO: Remove this back-compat after next major release
1261                 cls = OO.getProp.apply( OO, [ OO.ui ].concat( parts ) );
1262                 if ( cls === undefined ) {
1263                         throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
1264                 }
1265         }
1267         // Verify that we're creating an OO.ui.Element instance
1268         parent = cls.parent;
1270         while ( parent !== undefined ) {
1271                 if ( parent === OO.ui.Element ) {
1272                         // Safe
1273                         break;
1274                 }
1276                 parent = parent.parent;
1277         }
1279         if ( parent !== OO.ui.Element ) {
1280                 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
1281         }
1283         if ( domPromise === false ) {
1284                 top = $.Deferred();
1285                 domPromise = top.promise();
1286         }
1287         $elem.data( 'ooui-infused', true ); // prevent loops
1288         data.id = id; // implicit
1289         data = OO.copy( data, null, function deserialize( value ) {
1290                 if ( OO.isPlainObject( value ) ) {
1291                         if ( value.tag ) {
1292                                 return OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
1293                         }
1294                         if ( value.html ) {
1295                                 return new OO.ui.HtmlSnippet( value.html );
1296                         }
1297                 }
1298         } );
1299         // jscs:disable requireCapitalizedConstructors
1300         obj = new cls( data ); // rebuild widget
1301         // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
1302         state = obj.gatherPreInfuseState( $elem );
1303         // now replace old DOM with this new DOM.
1304         if ( top ) {
1305                 $elem.replaceWith( obj.$element );
1306                 // This element is now gone from the DOM, but if anyone is holding a reference to it,
1307                 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
1308                 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
1309                 $elem[ 0 ].oouiInfused = obj;
1310                 top.resolve();
1311         }
1312         obj.$element.data( 'ooui-infused', obj );
1313         // set the 'data-ooui' attribute so we can identify infused widgets
1314         obj.$element.attr( 'data-ooui', '' );
1315         // restore dynamic state after the new element is inserted into DOM
1316         domPromise.done( obj.restorePreInfuseState.bind( obj, state ) );
1317         return obj;
1321  * Get a jQuery function within a specific document.
1323  * @static
1324  * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
1325  * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
1326  *   not in an iframe
1327  * @return {Function} Bound jQuery function
1328  */
1329 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
1330         function wrapper( selector ) {
1331                 return $( selector, wrapper.context );
1332         }
1334         wrapper.context = this.getDocument( context );
1336         if ( $iframe ) {
1337                 wrapper.$iframe = $iframe;
1338         }
1340         return wrapper;
1344  * Get the document of an element.
1346  * @static
1347  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1348  * @return {HTMLDocument|null} Document object
1349  */
1350 OO.ui.Element.static.getDocument = function ( obj ) {
1351         // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1352         return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
1353                 // Empty jQuery selections might have a context
1354                 obj.context ||
1355                 // HTMLElement
1356                 obj.ownerDocument ||
1357                 // Window
1358                 obj.document ||
1359                 // HTMLDocument
1360                 ( obj.nodeType === 9 && obj ) ||
1361                 null;
1365  * Get the window of an element or document.
1367  * @static
1368  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1369  * @return {Window} Window object
1370  */
1371 OO.ui.Element.static.getWindow = function ( obj ) {
1372         var doc = this.getDocument( obj );
1373         // Support: IE 8
1374         // Standard Document.defaultView is IE9+
1375         return doc.parentWindow || doc.defaultView;
1379  * Get the direction of an element or document.
1381  * @static
1382  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1383  * @return {string} Text direction, either 'ltr' or 'rtl'
1384  */
1385 OO.ui.Element.static.getDir = function ( obj ) {
1386         var isDoc, isWin;
1388         if ( obj instanceof jQuery ) {
1389                 obj = obj[ 0 ];
1390         }
1391         isDoc = obj.nodeType === 9;
1392         isWin = obj.document !== undefined;
1393         if ( isDoc || isWin ) {
1394                 if ( isWin ) {
1395                         obj = obj.document;
1396                 }
1397                 obj = obj.body;
1398         }
1399         return $( obj ).css( 'direction' );
1403  * Get the offset between two frames.
1405  * TODO: Make this function not use recursion.
1407  * @static
1408  * @param {Window} from Window of the child frame
1409  * @param {Window} [to=window] Window of the parent frame
1410  * @param {Object} [offset] Offset to start with, used internally
1411  * @return {Object} Offset object, containing left and top properties
1412  */
1413 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
1414         var i, len, frames, frame, rect;
1416         if ( !to ) {
1417                 to = window;
1418         }
1419         if ( !offset ) {
1420                 offset = { top: 0, left: 0 };
1421         }
1422         if ( from.parent === from ) {
1423                 return offset;
1424         }
1426         // Get iframe element
1427         frames = from.parent.document.getElementsByTagName( 'iframe' );
1428         for ( i = 0, len = frames.length; i < len; i++ ) {
1429                 if ( frames[ i ].contentWindow === from ) {
1430                         frame = frames[ i ];
1431                         break;
1432                 }
1433         }
1435         // Recursively accumulate offset values
1436         if ( frame ) {
1437                 rect = frame.getBoundingClientRect();
1438                 offset.left += rect.left;
1439                 offset.top += rect.top;
1440                 if ( from !== to ) {
1441                         this.getFrameOffset( from.parent, offset );
1442                 }
1443         }
1444         return offset;
1448  * Get the offset between two elements.
1450  * The two elements may be in a different frame, but in that case the frame $element is in must
1451  * be contained in the frame $anchor is in.
1453  * @static
1454  * @param {jQuery} $element Element whose position to get
1455  * @param {jQuery} $anchor Element to get $element's position relative to
1456  * @return {Object} Translated position coordinates, containing top and left properties
1457  */
1458 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
1459         var iframe, iframePos,
1460                 pos = $element.offset(),
1461                 anchorPos = $anchor.offset(),
1462                 elementDocument = this.getDocument( $element ),
1463                 anchorDocument = this.getDocument( $anchor );
1465         // If $element isn't in the same document as $anchor, traverse up
1466         while ( elementDocument !== anchorDocument ) {
1467                 iframe = elementDocument.defaultView.frameElement;
1468                 if ( !iframe ) {
1469                         throw new Error( '$element frame is not contained in $anchor frame' );
1470                 }
1471                 iframePos = $( iframe ).offset();
1472                 pos.left += iframePos.left;
1473                 pos.top += iframePos.top;
1474                 elementDocument = iframe.ownerDocument;
1475         }
1476         pos.left -= anchorPos.left;
1477         pos.top -= anchorPos.top;
1478         return pos;
1482  * Get element border sizes.
1484  * @static
1485  * @param {HTMLElement} el Element to measure
1486  * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1487  */
1488 OO.ui.Element.static.getBorders = function ( el ) {
1489         var doc = el.ownerDocument,
1490                 // Support: IE 8
1491                 // Standard Document.defaultView is IE9+
1492                 win = doc.parentWindow || doc.defaultView,
1493                 style = win && win.getComputedStyle ?
1494                         win.getComputedStyle( el, null ) :
1495                         // Support: IE 8
1496                         // Standard getComputedStyle() is IE9+
1497                         el.currentStyle,
1498                 $el = $( el ),
1499                 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1500                 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1501                 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1502                 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1504         return {
1505                 top: top,
1506                 left: left,
1507                 bottom: bottom,
1508                 right: right
1509         };
1513  * Get dimensions of an element or window.
1515  * @static
1516  * @param {HTMLElement|Window} el Element to measure
1517  * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1518  */
1519 OO.ui.Element.static.getDimensions = function ( el ) {
1520         var $el, $win,
1521                 doc = el.ownerDocument || el.document,
1522                 // Support: IE 8
1523                 // Standard Document.defaultView is IE9+
1524                 win = doc.parentWindow || doc.defaultView;
1526         if ( win === el || el === doc.documentElement ) {
1527                 $win = $( win );
1528                 return {
1529                         borders: { top: 0, left: 0, bottom: 0, right: 0 },
1530                         scroll: {
1531                                 top: $win.scrollTop(),
1532                                 left: $win.scrollLeft()
1533                         },
1534                         scrollbar: { right: 0, bottom: 0 },
1535                         rect: {
1536                                 top: 0,
1537                                 left: 0,
1538                                 bottom: $win.innerHeight(),
1539                                 right: $win.innerWidth()
1540                         }
1541                 };
1542         } else {
1543                 $el = $( el );
1544                 return {
1545                         borders: this.getBorders( el ),
1546                         scroll: {
1547                                 top: $el.scrollTop(),
1548                                 left: $el.scrollLeft()
1549                         },
1550                         scrollbar: {
1551                                 right: $el.innerWidth() - el.clientWidth,
1552                                 bottom: $el.innerHeight() - el.clientHeight
1553                         },
1554                         rect: el.getBoundingClientRect()
1555                 };
1556         }
1560  * Get scrollable object parent
1562  * documentElement can't be used to get or set the scrollTop
1563  * property on Blink. Changing and testing its value lets us
1564  * use 'body' or 'documentElement' based on what is working.
1566  * https://code.google.com/p/chromium/issues/detail?id=303131
1568  * @static
1569  * @param {HTMLElement} el Element to find scrollable parent for
1570  * @return {HTMLElement} Scrollable parent
1571  */
1572 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1573         var scrollTop, body;
1575         if ( OO.ui.scrollableElement === undefined ) {
1576                 body = el.ownerDocument.body;
1577                 scrollTop = body.scrollTop;
1578                 body.scrollTop = 1;
1580                 if ( body.scrollTop === 1 ) {
1581                         body.scrollTop = scrollTop;
1582                         OO.ui.scrollableElement = 'body';
1583                 } else {
1584                         OO.ui.scrollableElement = 'documentElement';
1585                 }
1586         }
1588         return el.ownerDocument[ OO.ui.scrollableElement ];
1592  * Get closest scrollable container.
1594  * Traverses up until either a scrollable element or the root is reached, in which case the window
1595  * will be returned.
1597  * @static
1598  * @param {HTMLElement} el Element to find scrollable container for
1599  * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1600  * @return {HTMLElement} Closest scrollable container
1601  */
1602 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1603         var i, val,
1604                 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1605                 props = [ 'overflow-x', 'overflow-y' ],
1606                 $parent = $( el ).parent();
1608         if ( dimension === 'x' || dimension === 'y' ) {
1609                 props = [ 'overflow-' + dimension ];
1610         }
1612         while ( $parent.length ) {
1613                 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1614                         return $parent[ 0 ];
1615                 }
1616                 i = props.length;
1617                 while ( i-- ) {
1618                         val = $parent.css( props[ i ] );
1619                         if ( val === 'auto' || val === 'scroll' ) {
1620                                 return $parent[ 0 ];
1621                         }
1622                 }
1623                 $parent = $parent.parent();
1624         }
1625         return this.getDocument( el ).body;
1629  * Scroll element into view.
1631  * @static
1632  * @param {HTMLElement} el Element to scroll into view
1633  * @param {Object} [config] Configuration options
1634  * @param {string} [config.duration] jQuery animation duration value
1635  * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1636  *  to scroll in both directions
1637  * @param {Function} [config.complete] Function to call when scrolling completes
1638  */
1639 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1640         var rel, anim, callback, sc, $sc, eld, scd, $win;
1642         // Configuration initialization
1643         config = config || {};
1645         anim = {};
1646         callback = typeof config.complete === 'function' && config.complete;
1647         sc = this.getClosestScrollableContainer( el, config.direction );
1648         $sc = $( sc );
1649         eld = this.getDimensions( el );
1650         scd = this.getDimensions( sc );
1651         $win = $( this.getWindow( el ) );
1653         // Compute the distances between the edges of el and the edges of the scroll viewport
1654         if ( $sc.is( 'html, body' ) ) {
1655                 // If the scrollable container is the root, this is easy
1656                 rel = {
1657                         top: eld.rect.top,
1658                         bottom: $win.innerHeight() - eld.rect.bottom,
1659                         left: eld.rect.left,
1660                         right: $win.innerWidth() - eld.rect.right
1661                 };
1662         } else {
1663                 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1664                 rel = {
1665                         top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1666                         bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1667                         left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1668                         right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1669                 };
1670         }
1672         if ( !config.direction || config.direction === 'y' ) {
1673                 if ( rel.top < 0 ) {
1674                         anim.scrollTop = scd.scroll.top + rel.top;
1675                 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1676                         anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1677                 }
1678         }
1679         if ( !config.direction || config.direction === 'x' ) {
1680                 if ( rel.left < 0 ) {
1681                         anim.scrollLeft = scd.scroll.left + rel.left;
1682                 } else if ( rel.left > 0 && rel.right < 0 ) {
1683                         anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1684                 }
1685         }
1686         if ( !$.isEmptyObject( anim ) ) {
1687                 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1688                 if ( callback ) {
1689                         $sc.queue( function ( next ) {
1690                                 callback();
1691                                 next();
1692                         } );
1693                 }
1694         } else {
1695                 if ( callback ) {
1696                         callback();
1697                 }
1698         }
1702  * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1703  * and reserve space for them, because it probably doesn't.
1705  * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1706  * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1707  * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1708  * and then reattach (or show) them back.
1710  * @static
1711  * @param {HTMLElement} el Element to reconsider the scrollbars on
1712  */
1713 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1714         var i, len, scrollLeft, scrollTop, nodes = [];
1715         // Save scroll position
1716         scrollLeft = el.scrollLeft;
1717         scrollTop = el.scrollTop;
1718         // Detach all children
1719         while ( el.firstChild ) {
1720                 nodes.push( el.firstChild );
1721                 el.removeChild( el.firstChild );
1722         }
1723         // Force reflow
1724         void el.offsetHeight;
1725         // Reattach all children
1726         for ( i = 0, len = nodes.length; i < len; i++ ) {
1727                 el.appendChild( nodes[ i ] );
1728         }
1729         // Restore scroll position (no-op if scrollbars disappeared)
1730         el.scrollLeft = scrollLeft;
1731         el.scrollTop = scrollTop;
1734 /* Methods */
1737  * Toggle visibility of an element.
1739  * @param {boolean} [show] Make element visible, omit to toggle visibility
1740  * @fires visible
1741  * @chainable
1742  */
1743 OO.ui.Element.prototype.toggle = function ( show ) {
1744         show = show === undefined ? !this.visible : !!show;
1746         if ( show !== this.isVisible() ) {
1747                 this.visible = show;
1748                 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1749                 this.emit( 'toggle', show );
1750         }
1752         return this;
1756  * Check if element is visible.
1758  * @return {boolean} element is visible
1759  */
1760 OO.ui.Element.prototype.isVisible = function () {
1761         return this.visible;
1765  * Get element data.
1767  * @return {Mixed} Element data
1768  */
1769 OO.ui.Element.prototype.getData = function () {
1770         return this.data;
1774  * Set element data.
1776  * @param {Mixed} Element data
1777  * @chainable
1778  */
1779 OO.ui.Element.prototype.setData = function ( data ) {
1780         this.data = data;
1781         return this;
1785  * Check if element supports one or more methods.
1787  * @param {string|string[]} methods Method or list of methods to check
1788  * @return {boolean} All methods are supported
1789  */
1790 OO.ui.Element.prototype.supports = function ( methods ) {
1791         var i, len,
1792                 support = 0;
1794         methods = Array.isArray( methods ) ? methods : [ methods ];
1795         for ( i = 0, len = methods.length; i < len; i++ ) {
1796                 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1797                         support++;
1798                 }
1799         }
1801         return methods.length === support;
1805  * Update the theme-provided classes.
1807  * @localdoc This is called in element mixins and widget classes any time state changes.
1808  *   Updating is debounced, minimizing overhead of changing multiple attributes and
1809  *   guaranteeing that theme updates do not occur within an element's constructor
1810  */
1811 OO.ui.Element.prototype.updateThemeClasses = function () {
1812         this.debouncedUpdateThemeClassesHandler();
1816  * @private
1817  * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1818  *   make them synchronous.
1819  */
1820 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1821         OO.ui.theme.updateElementClasses( this );
1825  * Get the HTML tag name.
1827  * Override this method to base the result on instance information.
1829  * @return {string} HTML tag name
1830  */
1831 OO.ui.Element.prototype.getTagName = function () {
1832         return this.constructor.static.tagName;
1836  * Check if the element is attached to the DOM
1837  * @return {boolean} The element is attached to the DOM
1838  */
1839 OO.ui.Element.prototype.isElementAttached = function () {
1840         return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1844  * Get the DOM document.
1846  * @return {HTMLDocument} Document object
1847  */
1848 OO.ui.Element.prototype.getElementDocument = function () {
1849         // Don't cache this in other ways either because subclasses could can change this.$element
1850         return OO.ui.Element.static.getDocument( this.$element );
1854  * Get the DOM window.
1856  * @return {Window} Window object
1857  */
1858 OO.ui.Element.prototype.getElementWindow = function () {
1859         return OO.ui.Element.static.getWindow( this.$element );
1863  * Get closest scrollable container.
1864  */
1865 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1866         return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1870  * Get group element is in.
1872  * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1873  */
1874 OO.ui.Element.prototype.getElementGroup = function () {
1875         return this.elementGroup;
1879  * Set group element is in.
1881  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1882  * @chainable
1883  */
1884 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1885         this.elementGroup = group;
1886         return this;
1890  * Scroll element into view.
1892  * @param {Object} [config] Configuration options
1893  */
1894 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1895         return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1899  * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
1900  * (and its children) that represent an Element of the same type and configuration as the current
1901  * one, generated by the PHP implementation.
1903  * This method is called just before `node` is detached from the DOM. The return value of this
1904  * function will be passed to #restorePreInfuseState after this widget's #$element is inserted into
1905  * DOM to replace `node`.
1907  * @protected
1908  * @param {HTMLElement} node
1909  * @return {Object}
1910  */
1911 OO.ui.Element.prototype.gatherPreInfuseState = function () {
1912         return {};
1916  * Restore the pre-infusion dynamic state for this widget.
1918  * This method is called after #$element has been inserted into DOM. The parameter is the return
1919  * value of #gatherPreInfuseState.
1921  * @protected
1922  * @param {Object} state
1923  */
1924 OO.ui.Element.prototype.restorePreInfuseState = function () {
1928  * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1929  * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1930  * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1931  * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1932  * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1934  * @abstract
1935  * @class
1936  * @extends OO.ui.Element
1937  * @mixins OO.EventEmitter
1939  * @constructor
1940  * @param {Object} [config] Configuration options
1941  */
1942 OO.ui.Layout = function OoUiLayout( config ) {
1943         // Configuration initialization
1944         config = config || {};
1946         // Parent constructor
1947         OO.ui.Layout.parent.call( this, config );
1949         // Mixin constructors
1950         OO.EventEmitter.call( this );
1952         // Initialization
1953         this.$element.addClass( 'oo-ui-layout' );
1956 /* Setup */
1958 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1959 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1962  * Widgets are compositions of one or more OOjs UI elements that users can both view
1963  * and interact with. All widgets can be configured and modified via a standard API,
1964  * and their state can change dynamically according to a model.
1966  * @abstract
1967  * @class
1968  * @extends OO.ui.Element
1969  * @mixins OO.EventEmitter
1971  * @constructor
1972  * @param {Object} [config] Configuration options
1973  * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1974  *  appearance reflects this state.
1975  */
1976 OO.ui.Widget = function OoUiWidget( config ) {
1977         // Initialize config
1978         config = $.extend( { disabled: false }, config );
1980         // Parent constructor
1981         OO.ui.Widget.parent.call( this, config );
1983         // Mixin constructors
1984         OO.EventEmitter.call( this );
1986         // Properties
1987         this.disabled = null;
1988         this.wasDisabled = null;
1990         // Initialization
1991         this.$element.addClass( 'oo-ui-widget' );
1992         this.setDisabled( !!config.disabled );
1995 /* Setup */
1997 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1998 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
2000 /* Static Properties */
2003  * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
2004  * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
2005  * handling.
2007  * @static
2008  * @inheritable
2009  * @property {boolean}
2010  */
2011 OO.ui.Widget.static.supportsSimpleLabel = false;
2013 /* Events */
2016  * @event disable
2018  * A 'disable' event is emitted when the disabled state of the widget changes
2019  * (i.e. on disable **and** enable).
2021  * @param {boolean} disabled Widget is disabled
2022  */
2025  * @event toggle
2027  * A 'toggle' event is emitted when the visibility of the widget changes.
2029  * @param {boolean} visible Widget is visible
2030  */
2032 /* Methods */
2035  * Check if the widget is disabled.
2037  * @return {boolean} Widget is disabled
2038  */
2039 OO.ui.Widget.prototype.isDisabled = function () {
2040         return this.disabled;
2044  * Set the 'disabled' state of the widget.
2046  * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
2048  * @param {boolean} disabled Disable widget
2049  * @chainable
2050  */
2051 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
2052         var isDisabled;
2054         this.disabled = !!disabled;
2055         isDisabled = this.isDisabled();
2056         if ( isDisabled !== this.wasDisabled ) {
2057                 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
2058                 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
2059                 this.$element.attr( 'aria-disabled', isDisabled.toString() );
2060                 this.emit( 'disable', isDisabled );
2061                 this.updateThemeClasses();
2062         }
2063         this.wasDisabled = isDisabled;
2065         return this;
2069  * Update the disabled state, in case of changes in parent widget.
2071  * @chainable
2072  */
2073 OO.ui.Widget.prototype.updateDisabled = function () {
2074         this.setDisabled( this.disabled );
2075         return this;
2079  * A window is a container for elements that are in a child frame. They are used with
2080  * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
2081  * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
2082  * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
2083  * the window manager will choose a sensible fallback.
2085  * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
2086  * different processes are executed:
2088  * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
2089  * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
2090  * the window.
2092  * - {@link #getSetupProcess} method is called and its result executed
2093  * - {@link #getReadyProcess} method is called and its result executed
2095  * **opened**: The window is now open
2097  * **closing**: The closing stage begins when the window manager's
2098  * {@link OO.ui.WindowManager#closeWindow closeWindow}
2099  * or the window's {@link #close} methods are used, and the window manager begins to close the window.
2101  * - {@link #getHoldProcess} method is called and its result executed
2102  * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
2104  * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
2105  * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
2106  * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
2107  * processing can complete. Always assume window processes are executed asynchronously.
2109  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2111  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
2113  * @abstract
2114  * @class
2115  * @extends OO.ui.Element
2116  * @mixins OO.EventEmitter
2118  * @constructor
2119  * @param {Object} [config] Configuration options
2120  * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
2121  *  `full`.  If omitted, the value of the {@link #static-size static size} property will be used.
2122  */
2123 OO.ui.Window = function OoUiWindow( config ) {
2124         // Configuration initialization
2125         config = config || {};
2127         // Parent constructor
2128         OO.ui.Window.parent.call( this, config );
2130         // Mixin constructors
2131         OO.EventEmitter.call( this );
2133         // Properties
2134         this.manager = null;
2135         this.size = config.size || this.constructor.static.size;
2136         this.$frame = $( '<div>' );
2137         this.$overlay = $( '<div>' );
2138         this.$content = $( '<div>' );
2140         this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
2141         this.$focusTrapAfter = $( '<div>' ).prop( 'tabIndex', 0 );
2142         this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
2144         // Initialization
2145         this.$overlay.addClass( 'oo-ui-window-overlay' );
2146         this.$content
2147                 .addClass( 'oo-ui-window-content' )
2148                 .attr( 'tabindex', 0 );
2149         this.$frame
2150                 .addClass( 'oo-ui-window-frame' )
2151                 .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
2153         this.$element
2154                 .addClass( 'oo-ui-window' )
2155                 .append( this.$frame, this.$overlay );
2157         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
2158         // that reference properties not initialized at that time of parent class construction
2159         // TODO: Find a better way to handle post-constructor setup
2160         this.visible = false;
2161         this.$element.addClass( 'oo-ui-element-hidden' );
2164 /* Setup */
2166 OO.inheritClass( OO.ui.Window, OO.ui.Element );
2167 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
2169 /* Static Properties */
2172  * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
2174  * The static size is used if no #size is configured during construction.
2176  * @static
2177  * @inheritable
2178  * @property {string}
2179  */
2180 OO.ui.Window.static.size = 'medium';
2182 /* Methods */
2185  * Handle mouse down events.
2187  * @private
2188  * @param {jQuery.Event} e Mouse down event
2189  */
2190 OO.ui.Window.prototype.onMouseDown = function ( e ) {
2191         // Prevent clicking on the click-block from stealing focus
2192         if ( e.target === this.$element[ 0 ] ) {
2193                 return false;
2194         }
2198  * Check if the window has been initialized.
2200  * Initialization occurs when a window is added to a manager.
2202  * @return {boolean} Window has been initialized
2203  */
2204 OO.ui.Window.prototype.isInitialized = function () {
2205         return !!this.manager;
2209  * Check if the window is visible.
2211  * @return {boolean} Window is visible
2212  */
2213 OO.ui.Window.prototype.isVisible = function () {
2214         return this.visible;
2218  * Check if the window is opening.
2220  * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
2221  * method.
2223  * @return {boolean} Window is opening
2224  */
2225 OO.ui.Window.prototype.isOpening = function () {
2226         return this.manager.isOpening( this );
2230  * Check if the window is closing.
2232  * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
2234  * @return {boolean} Window is closing
2235  */
2236 OO.ui.Window.prototype.isClosing = function () {
2237         return this.manager.isClosing( this );
2241  * Check if the window is opened.
2243  * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
2245  * @return {boolean} Window is opened
2246  */
2247 OO.ui.Window.prototype.isOpened = function () {
2248         return this.manager.isOpened( this );
2252  * Get the window manager.
2254  * All windows must be attached to a window manager, which is used to open
2255  * and close the window and control its presentation.
2257  * @return {OO.ui.WindowManager} Manager of window
2258  */
2259 OO.ui.Window.prototype.getManager = function () {
2260         return this.manager;
2264  * Get the symbolic name of the window size (e.g., `small` or `medium`).
2266  * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
2267  */
2268 OO.ui.Window.prototype.getSize = function () {
2269         var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ),
2270                 sizes = this.manager.constructor.static.sizes,
2271                 size = this.size;
2273         if ( !sizes[ size ] ) {
2274                 size = this.manager.constructor.static.defaultSize;
2275         }
2276         if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
2277                 size = 'full';
2278         }
2280         return size;
2284  * Get the size properties associated with the current window size
2286  * @return {Object} Size properties
2287  */
2288 OO.ui.Window.prototype.getSizeProperties = function () {
2289         return this.manager.constructor.static.sizes[ this.getSize() ];
2293  * Disable transitions on window's frame for the duration of the callback function, then enable them
2294  * back.
2296  * @private
2297  * @param {Function} callback Function to call while transitions are disabled
2298  */
2299 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
2300         // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2301         // Disable transitions first, otherwise we'll get values from when the window was animating.
2302         var oldTransition,
2303                 styleObj = this.$frame[ 0 ].style;
2304         oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
2305                 styleObj.MozTransition || styleObj.WebkitTransition;
2306         styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2307                 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
2308         callback();
2309         // Force reflow to make sure the style changes done inside callback really are not transitioned
2310         this.$frame.height();
2311         styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2312                 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
2316  * Get the height of the full window contents (i.e., the window head, body and foot together).
2318  * What consistitutes the head, body, and foot varies depending on the window type.
2319  * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
2320  * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
2321  * and special actions in the head, and dialog content in the body.
2323  * To get just the height of the dialog body, use the #getBodyHeight method.
2325  * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
2326  */
2327 OO.ui.Window.prototype.getContentHeight = function () {
2328         var bodyHeight,
2329                 win = this,
2330                 bodyStyleObj = this.$body[ 0 ].style,
2331                 frameStyleObj = this.$frame[ 0 ].style;
2333         // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2334         // Disable transitions first, otherwise we'll get values from when the window was animating.
2335         this.withoutSizeTransitions( function () {
2336                 var oldHeight = frameStyleObj.height,
2337                         oldPosition = bodyStyleObj.position;
2338                 frameStyleObj.height = '1px';
2339                 // Force body to resize to new width
2340                 bodyStyleObj.position = 'relative';
2341                 bodyHeight = win.getBodyHeight();
2342                 frameStyleObj.height = oldHeight;
2343                 bodyStyleObj.position = oldPosition;
2344         } );
2346         return (
2347                 // Add buffer for border
2348                 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
2349                 // Use combined heights of children
2350                 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
2351         );
2355  * Get the height of the window body.
2357  * To get the height of the full window contents (the window body, head, and foot together),
2358  * use #getContentHeight.
2360  * When this function is called, the window will temporarily have been resized
2361  * to height=1px, so .scrollHeight measurements can be taken accurately.
2363  * @return {number} Height of the window body in pixels
2364  */
2365 OO.ui.Window.prototype.getBodyHeight = function () {
2366         return this.$body[ 0 ].scrollHeight;
2370  * Get the directionality of the frame (right-to-left or left-to-right).
2372  * @return {string} Directionality: `'ltr'` or `'rtl'`
2373  */
2374 OO.ui.Window.prototype.getDir = function () {
2375         return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2379  * Get the 'setup' process.
2381  * The setup process is used to set up a window for use in a particular context,
2382  * based on the `data` argument. This method is called during the opening phase of the window’s
2383  * lifecycle.
2385  * Override this method to add additional steps to the ‘setup’ process the parent method provides
2386  * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2387  * of OO.ui.Process.
2389  * To add window content that persists between openings, you may wish to use the #initialize method
2390  * instead.
2392  * @abstract
2393  * @param {Object} [data] Window opening data
2394  * @return {OO.ui.Process} Setup process
2395  */
2396 OO.ui.Window.prototype.getSetupProcess = function () {
2397         return new OO.ui.Process();
2401  * Get the ‘ready’ process.
2403  * The ready process is used to ready a window for use in a particular
2404  * context, based on the `data` argument. This method is called during the opening phase of
2405  * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
2407  * Override this method to add additional steps to the ‘ready’ process the parent method
2408  * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2409  * methods of OO.ui.Process.
2411  * @abstract
2412  * @param {Object} [data] Window opening data
2413  * @return {OO.ui.Process} Ready process
2414  */
2415 OO.ui.Window.prototype.getReadyProcess = function () {
2416         return new OO.ui.Process();
2420  * Get the 'hold' process.
2422  * The hold proccess is used to keep a window from being used in a particular context,
2423  * based on the `data` argument. This method is called during the closing phase of the window’s
2424  * lifecycle.
2426  * Override this method to add additional steps to the 'hold' process the parent method provides
2427  * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2428  * of OO.ui.Process.
2430  * @abstract
2431  * @param {Object} [data] Window closing data
2432  * @return {OO.ui.Process} Hold process
2433  */
2434 OO.ui.Window.prototype.getHoldProcess = function () {
2435         return new OO.ui.Process();
2439  * Get the ‘teardown’ process.
2441  * The teardown process is used to teardown a window after use. During teardown,
2442  * user interactions within the window are conveyed and the window is closed, based on the `data`
2443  * argument. This method is called during the closing phase of the window’s lifecycle.
2445  * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2446  * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2447  * of OO.ui.Process.
2449  * @abstract
2450  * @param {Object} [data] Window closing data
2451  * @return {OO.ui.Process} Teardown process
2452  */
2453 OO.ui.Window.prototype.getTeardownProcess = function () {
2454         return new OO.ui.Process();
2458  * Set the window manager.
2460  * This will cause the window to initialize. Calling it more than once will cause an error.
2462  * @param {OO.ui.WindowManager} manager Manager for this window
2463  * @throws {Error} An error is thrown if the method is called more than once
2464  * @chainable
2465  */
2466 OO.ui.Window.prototype.setManager = function ( manager ) {
2467         if ( this.manager ) {
2468                 throw new Error( 'Cannot set window manager, window already has a manager' );
2469         }
2471         this.manager = manager;
2472         this.initialize();
2474         return this;
2478  * Set the window size by symbolic name (e.g., 'small' or 'medium')
2480  * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2481  *  `full`
2482  * @chainable
2483  */
2484 OO.ui.Window.prototype.setSize = function ( size ) {
2485         this.size = size;
2486         this.updateSize();
2487         return this;
2491  * Update the window size.
2493  * @throws {Error} An error is thrown if the window is not attached to a window manager
2494  * @chainable
2495  */
2496 OO.ui.Window.prototype.updateSize = function () {
2497         if ( !this.manager ) {
2498                 throw new Error( 'Cannot update window size, must be attached to a manager' );
2499         }
2501         this.manager.updateWindowSize( this );
2503         return this;
2507  * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2508  * when the window is opening. In general, setDimensions should not be called directly.
2510  * To set the size of the window, use the #setSize method.
2512  * @param {Object} dim CSS dimension properties
2513  * @param {string|number} [dim.width] Width
2514  * @param {string|number} [dim.minWidth] Minimum width
2515  * @param {string|number} [dim.maxWidth] Maximum width
2516  * @param {string|number} [dim.width] Height, omit to set based on height of contents
2517  * @param {string|number} [dim.minWidth] Minimum height
2518  * @param {string|number} [dim.maxWidth] Maximum height
2519  * @chainable
2520  */
2521 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2522         var height,
2523                 win = this,
2524                 styleObj = this.$frame[ 0 ].style;
2526         // Calculate the height we need to set using the correct width
2527         if ( dim.height === undefined ) {
2528                 this.withoutSizeTransitions( function () {
2529                         var oldWidth = styleObj.width;
2530                         win.$frame.css( 'width', dim.width || '' );
2531                         height = win.getContentHeight();
2532                         styleObj.width = oldWidth;
2533                 } );
2534         } else {
2535                 height = dim.height;
2536         }
2538         this.$frame.css( {
2539                 width: dim.width || '',
2540                 minWidth: dim.minWidth || '',
2541                 maxWidth: dim.maxWidth || '',
2542                 height: height || '',
2543                 minHeight: dim.minHeight || '',
2544                 maxHeight: dim.maxHeight || ''
2545         } );
2547         return this;
2551  * Initialize window contents.
2553  * Before the window is opened for the first time, #initialize is called so that content that
2554  * persists between openings can be added to the window.
2556  * To set up a window with new content each time the window opens, use #getSetupProcess.
2558  * @throws {Error} An error is thrown if the window is not attached to a window manager
2559  * @chainable
2560  */
2561 OO.ui.Window.prototype.initialize = function () {
2562         if ( !this.manager ) {
2563                 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2564         }
2566         // Properties
2567         this.$head = $( '<div>' );
2568         this.$body = $( '<div>' );
2569         this.$foot = $( '<div>' );
2570         this.$document = $( this.getElementDocument() );
2572         // Events
2573         this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2575         // Initialization
2576         this.$head.addClass( 'oo-ui-window-head' );
2577         this.$body.addClass( 'oo-ui-window-body' );
2578         this.$foot.addClass( 'oo-ui-window-foot' );
2579         this.$content.append( this.$head, this.$body, this.$foot );
2581         return this;
2585  * Called when someone tries to focus the hidden element at the end of the dialog.
2586  * Sends focus back to the start of the dialog.
2588  * @param {jQuery.Event} event Focus event
2589  */
2590 OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
2591         if ( this.$focusTrapBefore.is( event.target ) ) {
2592                 OO.ui.findFocusable( this.$content, true ).focus();
2593         } else {
2594                 // this.$content is the part of the focus cycle, and is the first focusable element
2595                 this.$content.focus();
2596         }
2600  * Open the window.
2602  * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2603  * method, which returns a promise resolved when the window is done opening.
2605  * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2607  * @param {Object} [data] Window opening data
2608  * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2609  *  if the window fails to open. When the promise is resolved successfully, the first argument of the
2610  *  value is a new promise, which is resolved when the window begins closing.
2611  * @throws {Error} An error is thrown if the window is not attached to a window manager
2612  */
2613 OO.ui.Window.prototype.open = function ( data ) {
2614         if ( !this.manager ) {
2615                 throw new Error( 'Cannot open window, must be attached to a manager' );
2616         }
2618         return this.manager.openWindow( this, data );
2622  * Close the window.
2624  * This method is a wrapper around a call to the window
2625  * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2626  * which returns a closing promise resolved when the window is done closing.
2628  * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2629  * phase of the window’s lifecycle and can be used to specify closing behavior each time
2630  * the window closes.
2632  * @param {Object} [data] Window closing data
2633  * @return {jQuery.Promise} Promise resolved when window is closed
2634  * @throws {Error} An error is thrown if the window is not attached to a window manager
2635  */
2636 OO.ui.Window.prototype.close = function ( data ) {
2637         if ( !this.manager ) {
2638                 throw new Error( 'Cannot close window, must be attached to a manager' );
2639         }
2641         return this.manager.closeWindow( this, data );
2645  * Setup window.
2647  * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2648  * by other systems.
2650  * @param {Object} [data] Window opening data
2651  * @return {jQuery.Promise} Promise resolved when window is setup
2652  */
2653 OO.ui.Window.prototype.setup = function ( data ) {
2654         var win = this,
2655                 deferred = $.Deferred();
2657         this.toggle( true );
2659         this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
2660         this.$focusTraps.on( 'focus', this.focusTrapHandler );
2662         this.getSetupProcess( data ).execute().done( function () {
2663                 // Force redraw by asking the browser to measure the elements' widths
2664                 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2665                 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2666                 deferred.resolve();
2667         } );
2669         return deferred.promise();
2673  * Ready window.
2675  * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2676  * by other systems.
2678  * @param {Object} [data] Window opening data
2679  * @return {jQuery.Promise} Promise resolved when window is ready
2680  */
2681 OO.ui.Window.prototype.ready = function ( data ) {
2682         var win = this,
2683                 deferred = $.Deferred();
2685         this.$content.focus();
2686         this.getReadyProcess( data ).execute().done( function () {
2687                 // Force redraw by asking the browser to measure the elements' widths
2688                 win.$element.addClass( 'oo-ui-window-ready' ).width();
2689                 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2690                 deferred.resolve();
2691         } );
2693         return deferred.promise();
2697  * Hold window.
2699  * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2700  * by other systems.
2702  * @param {Object} [data] Window closing data
2703  * @return {jQuery.Promise} Promise resolved when window is held
2704  */
2705 OO.ui.Window.prototype.hold = function ( data ) {
2706         var win = this,
2707                 deferred = $.Deferred();
2709         this.getHoldProcess( data ).execute().done( function () {
2710                 // Get the focused element within the window's content
2711                 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2713                 // Blur the focused element
2714                 if ( $focus.length ) {
2715                         $focus[ 0 ].blur();
2716                 }
2718                 // Force redraw by asking the browser to measure the elements' widths
2719                 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2720                 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2721                 deferred.resolve();
2722         } );
2724         return deferred.promise();
2728  * Teardown window.
2730  * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2731  * by other systems.
2733  * @param {Object} [data] Window closing data
2734  * @return {jQuery.Promise} Promise resolved when window is torn down
2735  */
2736 OO.ui.Window.prototype.teardown = function ( data ) {
2737         var win = this;
2739         return this.getTeardownProcess( data ).execute()
2740                 .done( function () {
2741                         // Force redraw by asking the browser to measure the elements' widths
2742                         win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2743                         win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2744                         win.$focusTraps.off( 'focus', win.focusTrapHandler );
2745                         win.toggle( false );
2746                 } );
2750  * The Dialog class serves as the base class for the other types of dialogs.
2751  * Unless extended to include controls, the rendered dialog box is a simple window
2752  * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2753  * which opens, closes, and controls the presentation of the window. See the
2754  * [OOjs UI documentation on MediaWiki] [1] for more information.
2756  *     @example
2757  *     // A simple dialog window.
2758  *     function MyDialog( config ) {
2759  *         MyDialog.parent.call( this, config );
2760  *     }
2761  *     OO.inheritClass( MyDialog, OO.ui.Dialog );
2762  *     MyDialog.prototype.initialize = function () {
2763  *         MyDialog.parent.prototype.initialize.call( this );
2764  *         this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2765  *         this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2766  *         this.$body.append( this.content.$element );
2767  *     };
2768  *     MyDialog.prototype.getBodyHeight = function () {
2769  *         return this.content.$element.outerHeight( true );
2770  *     };
2771  *     var myDialog = new MyDialog( {
2772  *         size: 'medium'
2773  *     } );
2774  *     // Create and append a window manager, which opens and closes the window.
2775  *     var windowManager = new OO.ui.WindowManager();
2776  *     $( 'body' ).append( windowManager.$element );
2777  *     windowManager.addWindows( [ myDialog ] );
2778  *     // Open the window!
2779  *     windowManager.openWindow( myDialog );
2781  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2783  * @abstract
2784  * @class
2785  * @extends OO.ui.Window
2786  * @mixins OO.ui.mixin.PendingElement
2788  * @constructor
2789  * @param {Object} [config] Configuration options
2790  */
2791 OO.ui.Dialog = function OoUiDialog( config ) {
2792         // Parent constructor
2793         OO.ui.Dialog.parent.call( this, config );
2795         // Mixin constructors
2796         OO.ui.mixin.PendingElement.call( this );
2798         // Properties
2799         this.actions = new OO.ui.ActionSet();
2800         this.attachedActions = [];
2801         this.currentAction = null;
2802         this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
2804         // Events
2805         this.actions.connect( this, {
2806                 click: 'onActionClick',
2807                 resize: 'onActionResize',
2808                 change: 'onActionsChange'
2809         } );
2811         // Initialization
2812         this.$element
2813                 .addClass( 'oo-ui-dialog' )
2814                 .attr( 'role', 'dialog' );
2817 /* Setup */
2819 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2820 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2822 /* Static Properties */
2825  * Symbolic name of dialog.
2827  * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2828  * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2830  * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2832  * @abstract
2833  * @static
2834  * @inheritable
2835  * @property {string}
2836  */
2837 OO.ui.Dialog.static.name = '';
2840  * The dialog title.
2842  * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
2843  * that will produce a Label node or string. The title can also be specified with data passed to the
2844  * constructor (see #getSetupProcess). In this case, the static value will be overriden.
2846  * @abstract
2847  * @static
2848  * @inheritable
2849  * @property {jQuery|string|Function}
2850  */
2851 OO.ui.Dialog.static.title = '';
2854  * An array of configured {@link OO.ui.ActionWidget action widgets}.
2856  * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2857  * value will be overriden.
2859  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2861  * @static
2862  * @inheritable
2863  * @property {Object[]}
2864  */
2865 OO.ui.Dialog.static.actions = [];
2868  * Close the dialog when the 'Esc' key is pressed.
2870  * @static
2871  * @abstract
2872  * @inheritable
2873  * @property {boolean}
2874  */
2875 OO.ui.Dialog.static.escapable = true;
2877 /* Methods */
2880  * Handle frame document key down events.
2882  * @private
2883  * @param {jQuery.Event} e Key down event
2884  */
2885 OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
2886         if ( e.which === OO.ui.Keys.ESCAPE ) {
2887                 this.close();
2888                 e.preventDefault();
2889                 e.stopPropagation();
2890         }
2894  * Handle action resized events.
2896  * @private
2897  * @param {OO.ui.ActionWidget} action Action that was resized
2898  */
2899 OO.ui.Dialog.prototype.onActionResize = function () {
2900         // Override in subclass
2904  * Handle action click events.
2906  * @private
2907  * @param {OO.ui.ActionWidget} action Action that was clicked
2908  */
2909 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2910         if ( !this.isPending() ) {
2911                 this.executeAction( action.getAction() );
2912         }
2916  * Handle actions change event.
2918  * @private
2919  */
2920 OO.ui.Dialog.prototype.onActionsChange = function () {
2921         this.detachActions();
2922         if ( !this.isClosing() ) {
2923                 this.attachActions();
2924         }
2928  * Get the set of actions used by the dialog.
2930  * @return {OO.ui.ActionSet}
2931  */
2932 OO.ui.Dialog.prototype.getActions = function () {
2933         return this.actions;
2937  * Get a process for taking action.
2939  * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2940  * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2941  * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2943  * @abstract
2944  * @param {string} [action] Symbolic name of action
2945  * @return {OO.ui.Process} Action process
2946  */
2947 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2948         return new OO.ui.Process()
2949                 .next( function () {
2950                         if ( !action ) {
2951                                 // An empty action always closes the dialog without data, which should always be
2952                                 // safe and make no changes
2953                                 this.close();
2954                         }
2955                 }, this );
2959  * @inheritdoc
2961  * @param {Object} [data] Dialog opening data
2962  * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2963  *  the {@link #static-title static title}
2964  * @param {Object[]} [data.actions] List of configuration options for each
2965  *   {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2966  */
2967 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2968         data = data || {};
2970         // Parent method
2971         return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
2972                 .next( function () {
2973                         var config = this.constructor.static,
2974                                 actions = data.actions !== undefined ? data.actions : config.actions;
2976                         this.title.setLabel(
2977                                 data.title !== undefined ? data.title : this.constructor.static.title
2978                         );
2979                         this.actions.add( this.getActionWidgets( actions ) );
2981                         if ( this.constructor.static.escapable ) {
2982                                 this.$element.on( 'keydown', this.onDialogKeyDownHandler );
2983                         }
2984                 }, this );
2988  * @inheritdoc
2989  */
2990 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2991         // Parent method
2992         return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
2993                 .first( function () {
2994                         if ( this.constructor.static.escapable ) {
2995                                 this.$element.off( 'keydown', this.onDialogKeyDownHandler );
2996                         }
2998                         this.actions.clear();
2999                         this.currentAction = null;
3000                 }, this );
3004  * @inheritdoc
3005  */
3006 OO.ui.Dialog.prototype.initialize = function () {
3007         var titleId;
3009         // Parent method
3010         OO.ui.Dialog.parent.prototype.initialize.call( this );
3012         titleId = OO.ui.generateElementId();
3014         // Properties
3015         this.title = new OO.ui.LabelWidget( {
3016                 id: titleId
3017         } );
3019         // Initialization
3020         this.$content.addClass( 'oo-ui-dialog-content' );
3021         this.$element.attr( 'aria-labelledby', titleId );
3022         this.setPendingElement( this.$head );
3026  * Get action widgets from a list of configs
3028  * @param {Object[]} actions Action widget configs
3029  * @return {OO.ui.ActionWidget[]} Action widgets
3030  */
3031 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
3032         var i, len, widgets = [];
3033         for ( i = 0, len = actions.length; i < len; i++ ) {
3034                 widgets.push(
3035                         new OO.ui.ActionWidget( actions[ i ] )
3036                 );
3037         }
3038         return widgets;
3042  * Attach action actions.
3044  * @protected
3045  */
3046 OO.ui.Dialog.prototype.attachActions = function () {
3047         // Remember the list of potentially attached actions
3048         this.attachedActions = this.actions.get();
3052  * Detach action actions.
3054  * @protected
3055  * @chainable
3056  */
3057 OO.ui.Dialog.prototype.detachActions = function () {
3058         var i, len;
3060         // Detach all actions that may have been previously attached
3061         for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
3062                 this.attachedActions[ i ].$element.detach();
3063         }
3064         this.attachedActions = [];
3068  * Execute an action.
3070  * @param {string} action Symbolic name of action to execute
3071  * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
3072  */
3073 OO.ui.Dialog.prototype.executeAction = function ( action ) {
3074         this.pushPending();
3075         this.currentAction = action;
3076         return this.getActionProcess( action ).execute()
3077                 .always( this.popPending.bind( this ) );
3081  * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
3082  * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
3083  * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
3084  * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
3085  * pertinent data and reused.
3087  * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
3088  * `opened`, and `closing`, which represent the primary stages of the cycle:
3090  * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
3091  * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
3093  * - an `opening` event is emitted with an `opening` promise
3094  * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
3095  *   the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
3096  *   window and its result executed
3097  * - a `setup` progress notification is emitted from the `opening` promise
3098  * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
3099  *   the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
3100  *   window and its result executed
3101  * - a `ready` progress notification is emitted from the `opening` promise
3102  * - the `opening` promise is resolved with an `opened` promise
3104  * **Opened**: the window is now open.
3106  * **Closing**: the closing stage begins when the window manager's #closeWindow or the
3107  * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
3108  * to close the window.
3110  * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
3111  * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
3112  *   the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
3113  *   window and its result executed
3114  * - a `hold` progress notification is emitted from the `closing` promise
3115  * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
3116  *   the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
3117  *   window and its result executed
3118  * - a `teardown` progress notification is emitted from the `closing` promise
3119  * - the `closing` promise is resolved. The window is now closed
3121  * See the [OOjs UI documentation on MediaWiki][1] for more information.
3123  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3125  * @class
3126  * @extends OO.ui.Element
3127  * @mixins OO.EventEmitter
3129  * @constructor
3130  * @param {Object} [config] Configuration options
3131  * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
3132  *  Note that window classes that are instantiated with a factory must have
3133  *  a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
3134  * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
3135  */
3136 OO.ui.WindowManager = function OoUiWindowManager( config ) {
3137         // Configuration initialization
3138         config = config || {};
3140         // Parent constructor
3141         OO.ui.WindowManager.parent.call( this, config );
3143         // Mixin constructors
3144         OO.EventEmitter.call( this );
3146         // Properties
3147         this.factory = config.factory;
3148         this.modal = config.modal === undefined || !!config.modal;
3149         this.windows = {};
3150         this.opening = null;
3151         this.opened = null;
3152         this.closing = null;
3153         this.preparingToOpen = null;
3154         this.preparingToClose = null;
3155         this.currentWindow = null;
3156         this.globalEvents = false;
3157         this.$ariaHidden = null;
3158         this.onWindowResizeTimeout = null;
3159         this.onWindowResizeHandler = this.onWindowResize.bind( this );
3160         this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
3162         // Initialization
3163         this.$element
3164                 .addClass( 'oo-ui-windowManager' )
3165                 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
3168 /* Setup */
3170 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
3171 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
3173 /* Events */
3176  * An 'opening' event is emitted when the window begins to be opened.
3178  * @event opening
3179  * @param {OO.ui.Window} win Window that's being opened
3180  * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
3181  *  When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
3182  *  is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
3183  * @param {Object} data Window opening data
3184  */
3187  * A 'closing' event is emitted when the window begins to be closed.
3189  * @event closing
3190  * @param {OO.ui.Window} win Window that's being closed
3191  * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
3192  *  is closed successfully. The promise emits `hold` and `teardown` notifications when those
3193  *  processes are complete. When the `closing` promise is resolved, the first argument of its value
3194  *  is the closing data.
3195  * @param {Object} data Window closing data
3196  */
3199  * A 'resize' event is emitted when a window is resized.
3201  * @event resize
3202  * @param {OO.ui.Window} win Window that was resized
3203  */
3205 /* Static Properties */
3208  * Map of the symbolic name of each window size and its CSS properties.
3210  * @static
3211  * @inheritable
3212  * @property {Object}
3213  */
3214 OO.ui.WindowManager.static.sizes = {
3215         small: {
3216                 width: 300
3217         },
3218         medium: {
3219                 width: 500
3220         },
3221         large: {
3222                 width: 700
3223         },
3224         larger: {
3225                 width: 900
3226         },
3227         full: {
3228                 // These can be non-numeric because they are never used in calculations
3229                 width: '100%',
3230                 height: '100%'
3231         }
3235  * Symbolic name of the default window size.
3237  * The default size is used if the window's requested size is not recognized.
3239  * @static
3240  * @inheritable
3241  * @property {string}
3242  */
3243 OO.ui.WindowManager.static.defaultSize = 'medium';
3245 /* Methods */
3248  * Handle window resize events.
3250  * @private
3251  * @param {jQuery.Event} e Window resize event
3252  */
3253 OO.ui.WindowManager.prototype.onWindowResize = function () {
3254         clearTimeout( this.onWindowResizeTimeout );
3255         this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
3259  * Handle window resize events.
3261  * @private
3262  * @param {jQuery.Event} e Window resize event
3263  */
3264 OO.ui.WindowManager.prototype.afterWindowResize = function () {
3265         if ( this.currentWindow ) {
3266                 this.updateWindowSize( this.currentWindow );
3267         }
3271  * Check if window is opening.
3273  * @return {boolean} Window is opening
3274  */
3275 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
3276         return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
3280  * Check if window is closing.
3282  * @return {boolean} Window is closing
3283  */
3284 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
3285         return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
3289  * Check if window is opened.
3291  * @return {boolean} Window is opened
3292  */
3293 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
3294         return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
3298  * Check if a window is being managed.
3300  * @param {OO.ui.Window} win Window to check
3301  * @return {boolean} Window is being managed
3302  */
3303 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
3304         var name;
3306         for ( name in this.windows ) {
3307                 if ( this.windows[ name ] === win ) {
3308                         return true;
3309                 }
3310         }
3312         return false;
3316  * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
3318  * @param {OO.ui.Window} win Window being opened
3319  * @param {Object} [data] Window opening data
3320  * @return {number} Milliseconds to wait
3321  */
3322 OO.ui.WindowManager.prototype.getSetupDelay = function () {
3323         return 0;
3327  * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
3329  * @param {OO.ui.Window} win Window being opened
3330  * @param {Object} [data] Window opening data
3331  * @return {number} Milliseconds to wait
3332  */
3333 OO.ui.WindowManager.prototype.getReadyDelay = function () {
3334         return 0;
3338  * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
3340  * @param {OO.ui.Window} win Window being closed
3341  * @param {Object} [data] Window closing data
3342  * @return {number} Milliseconds to wait
3343  */
3344 OO.ui.WindowManager.prototype.getHoldDelay = function () {
3345         return 0;
3349  * Get the number of milliseconds to wait after the ‘hold’ process has finished before
3350  * executing the ‘teardown’ process.
3352  * @param {OO.ui.Window} win Window being closed
3353  * @param {Object} [data] Window closing data
3354  * @return {number} Milliseconds to wait
3355  */
3356 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
3357         return this.modal ? 250 : 0;
3361  * Get a window by its symbolic name.
3363  * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
3364  * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
3365  * for more information about using factories.
3366  * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3368  * @param {string} name Symbolic name of the window
3369  * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
3370  * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
3371  * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
3372  */
3373 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
3374         var deferred = $.Deferred(),
3375                 win = this.windows[ name ];
3377         if ( !( win instanceof OO.ui.Window ) ) {
3378                 if ( this.factory ) {
3379                         if ( !this.factory.lookup( name ) ) {
3380                                 deferred.reject( new OO.ui.Error(
3381                                         'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
3382                                 ) );
3383                         } else {
3384                                 win = this.factory.create( name );
3385                                 this.addWindows( [ win ] );
3386                                 deferred.resolve( win );
3387                         }
3388                 } else {
3389                         deferred.reject( new OO.ui.Error(
3390                                 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
3391                         ) );
3392                 }
3393         } else {
3394                 deferred.resolve( win );
3395         }
3397         return deferred.promise();
3401  * Get current window.
3403  * @return {OO.ui.Window|null} Currently opening/opened/closing window
3404  */
3405 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
3406         return this.currentWindow;
3410  * Open a window.
3412  * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
3413  * @param {Object} [data] Window opening data
3414  * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
3415  *  See {@link #event-opening 'opening' event}  for more information about `opening` promises.
3416  * @fires opening
3417  */
3418 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
3419         var manager = this,
3420                 opening = $.Deferred();
3422         // Argument handling
3423         if ( typeof win === 'string' ) {
3424                 return this.getWindow( win ).then( function ( win ) {
3425                         return manager.openWindow( win, data );
3426                 } );
3427         }
3429         // Error handling
3430         if ( !this.hasWindow( win ) ) {
3431                 opening.reject( new OO.ui.Error(
3432                         'Cannot open window: window is not attached to manager'
3433                 ) );
3434         } else if ( this.preparingToOpen || this.opening || this.opened ) {
3435                 opening.reject( new OO.ui.Error(
3436                         'Cannot open window: another window is opening or open'
3437                 ) );
3438         }
3440         // Window opening
3441         if ( opening.state() !== 'rejected' ) {
3442                 // If a window is currently closing, wait for it to complete
3443                 this.preparingToOpen = $.when( this.closing );
3444                 // Ensure handlers get called after preparingToOpen is set
3445                 this.preparingToOpen.done( function () {
3446                         if ( manager.modal ) {
3447                                 manager.toggleGlobalEvents( true );
3448                                 manager.toggleAriaIsolation( true );
3449                         }
3450                         manager.currentWindow = win;
3451                         manager.opening = opening;
3452                         manager.preparingToOpen = null;
3453                         manager.emit( 'opening', win, opening, data );
3454                         setTimeout( function () {
3455                                 win.setup( data ).then( function () {
3456                                         manager.updateWindowSize( win );
3457                                         manager.opening.notify( { state: 'setup' } );
3458                                         setTimeout( function () {
3459                                                 win.ready( data ).then( function () {
3460                                                         manager.opening.notify( { state: 'ready' } );
3461                                                         manager.opening = null;
3462                                                         manager.opened = $.Deferred();
3463                                                         opening.resolve( manager.opened.promise(), data );
3464                                                 } );
3465                                         }, manager.getReadyDelay() );
3466                                 } );
3467                         }, manager.getSetupDelay() );
3468                 } );
3469         }
3471         return opening.promise();
3475  * Close a window.
3477  * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3478  * @param {Object} [data] Window closing data
3479  * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
3480  *  See {@link #event-closing 'closing' event} for more information about closing promises.
3481  * @throws {Error} An error is thrown if the window is not managed by the window manager.
3482  * @fires closing
3483  */
3484 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
3485         var manager = this,
3486                 closing = $.Deferred(),
3487                 opened;
3489         // Argument handling
3490         if ( typeof win === 'string' ) {
3491                 win = this.windows[ win ];
3492         } else if ( !this.hasWindow( win ) ) {
3493                 win = null;
3494         }
3496         // Error handling
3497         if ( !win ) {
3498                 closing.reject( new OO.ui.Error(
3499                         'Cannot close window: window is not attached to manager'
3500                 ) );
3501         } else if ( win !== this.currentWindow ) {
3502                 closing.reject( new OO.ui.Error(
3503                         'Cannot close window: window already closed with different data'
3504                 ) );
3505         } else if ( this.preparingToClose || this.closing ) {
3506                 closing.reject( new OO.ui.Error(
3507                         'Cannot close window: window already closing with different data'
3508                 ) );
3509         }
3511         // Window closing
3512         if ( closing.state() !== 'rejected' ) {
3513                 // If the window is currently opening, close it when it's done
3514                 this.preparingToClose = $.when( this.opening );
3515                 // Ensure handlers get called after preparingToClose is set
3516                 this.preparingToClose.done( function () {
3517                         manager.closing = closing;
3518                         manager.preparingToClose = null;
3519                         manager.emit( 'closing', win, closing, data );
3520                         opened = manager.opened;
3521                         manager.opened = null;
3522                         opened.resolve( closing.promise(), data );
3523                         setTimeout( function () {
3524                                 win.hold( data ).then( function () {
3525                                         closing.notify( { state: 'hold' } );
3526                                         setTimeout( function () {
3527                                                 win.teardown( data ).then( function () {
3528                                                         closing.notify( { state: 'teardown' } );
3529                                                         if ( manager.modal ) {
3530                                                                 manager.toggleGlobalEvents( false );
3531                                                                 manager.toggleAriaIsolation( false );
3532                                                         }
3533                                                         manager.closing = null;
3534                                                         manager.currentWindow = null;
3535                                                         closing.resolve( data );
3536                                                 } );
3537                                         }, manager.getTeardownDelay() );
3538                                 } );
3539                         }, manager.getHoldDelay() );
3540                 } );
3541         }
3543         return closing.promise();
3547  * Add windows to the window manager.
3549  * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
3550  * See the [OOjs ui documentation on MediaWiki] [2] for examples.
3551  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3553  * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
3554  *  by reference, symbolic name, or explicitly defined symbolic names.
3555  * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
3556  *  explicit nor a statically configured symbolic name.
3557  */
3558 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3559         var i, len, win, name, list;
3561         if ( Array.isArray( windows ) ) {
3562                 // Convert to map of windows by looking up symbolic names from static configuration
3563                 list = {};
3564                 for ( i = 0, len = windows.length; i < len; i++ ) {
3565                         name = windows[ i ].constructor.static.name;
3566                         if ( typeof name !== 'string' ) {
3567                                 throw new Error( 'Cannot add window' );
3568                         }
3569                         list[ name ] = windows[ i ];
3570                 }
3571         } else if ( OO.isPlainObject( windows ) ) {
3572                 list = windows;
3573         }
3575         // Add windows
3576         for ( name in list ) {
3577                 win = list[ name ];
3578                 this.windows[ name ] = win.toggle( false );
3579                 this.$element.append( win.$element );
3580                 win.setManager( this );
3581         }
3585  * Remove the specified windows from the windows manager.
3587  * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
3588  * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
3589  * longer listens to events, use the #destroy method.
3591  * @param {string[]} names Symbolic names of windows to remove
3592  * @return {jQuery.Promise} Promise resolved when window is closed and removed
3593  * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
3594  */
3595 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3596         var i, len, win, name, cleanupWindow,
3597                 manager = this,
3598                 promises = [],
3599                 cleanup = function ( name, win ) {
3600                         delete manager.windows[ name ];
3601                         win.$element.detach();
3602                 };
3604         for ( i = 0, len = names.length; i < len; i++ ) {
3605                 name = names[ i ];
3606                 win = this.windows[ name ];
3607                 if ( !win ) {
3608                         throw new Error( 'Cannot remove window' );
3609                 }
3610                 cleanupWindow = cleanup.bind( null, name, win );
3611                 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3612         }
3614         return $.when.apply( $, promises );
3618  * Remove all windows from the window manager.
3620  * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
3621  * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
3622  * To remove just a subset of windows, use the #removeWindows method.
3624  * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3625  */
3626 OO.ui.WindowManager.prototype.clearWindows = function () {
3627         return this.removeWindows( Object.keys( this.windows ) );
3631  * Set dialog size. In general, this method should not be called directly.
3633  * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3635  * @chainable
3636  */
3637 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3638         var isFullscreen;
3640         // Bypass for non-current, and thus invisible, windows
3641         if ( win !== this.currentWindow ) {
3642                 return;
3643         }
3645         isFullscreen = win.getSize() === 'full';
3647         this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
3648         this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
3649         win.setDimensions( win.getSizeProperties() );
3651         this.emit( 'resize', win );
3653         return this;
3657  * Bind or unbind global events for scrolling.
3659  * @private
3660  * @param {boolean} [on] Bind global events
3661  * @chainable
3662  */
3663 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3664         var scrollWidth, bodyMargin,
3665                 $body = $( this.getElementDocument().body ),
3666                 // We could have multiple window managers open so only modify
3667                 // the body css at the bottom of the stack
3668                 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
3670         on = on === undefined ? !!this.globalEvents : !!on;
3672         if ( on ) {
3673                 if ( !this.globalEvents ) {
3674                         $( this.getElementWindow() ).on( {
3675                                 // Start listening for top-level window dimension changes
3676                                 'orientationchange resize': this.onWindowResizeHandler
3677                         } );
3678                         if ( stackDepth === 0 ) {
3679                                 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
3680                                 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
3681                                 $body.css( {
3682                                         overflow: 'hidden',
3683                                         'margin-right': bodyMargin + scrollWidth
3684                                 } );
3685                         }
3686                         stackDepth++;
3687                         this.globalEvents = true;
3688                 }
3689         } else if ( this.globalEvents ) {
3690                 $( this.getElementWindow() ).off( {
3691                         // Stop listening for top-level window dimension changes
3692                         'orientationchange resize': this.onWindowResizeHandler
3693                 } );
3694                 stackDepth--;
3695                 if ( stackDepth === 0 ) {
3696                         $body.css( {
3697                                 overflow: '',
3698                                 'margin-right': ''
3699                         } );
3700                 }
3701                 this.globalEvents = false;
3702         }
3703         $body.data( 'windowManagerGlobalEvents', stackDepth );
3705         return this;
3709  * Toggle screen reader visibility of content other than the window manager.
3711  * @private
3712  * @param {boolean} [isolate] Make only the window manager visible to screen readers
3713  * @chainable
3714  */
3715 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3716         isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3718         if ( isolate ) {
3719                 if ( !this.$ariaHidden ) {
3720                         // Hide everything other than the window manager from screen readers
3721                         this.$ariaHidden = $( 'body' )
3722                                 .children()
3723                                 .not( this.$element.parentsUntil( 'body' ).last() )
3724                                 .attr( 'aria-hidden', '' );
3725                 }
3726         } else if ( this.$ariaHidden ) {
3727                 // Restore screen reader visibility
3728                 this.$ariaHidden.removeAttr( 'aria-hidden' );
3729                 this.$ariaHidden = null;
3730         }
3732         return this;
3736  * Destroy the window manager.
3738  * Destroying the window manager ensures that it will no longer listen to events. If you would like to
3739  * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
3740  * instead.
3741  */
3742 OO.ui.WindowManager.prototype.destroy = function () {
3743         this.toggleGlobalEvents( false );
3744         this.toggleAriaIsolation( false );
3745         this.clearWindows();
3746         this.$element.remove();
3750  * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
3751  * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
3752  * appearance and functionality of the error interface.
3754  * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
3755  * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
3756  * that initiated the failed process will be disabled.
3758  * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
3759  * process again.
3761  * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
3763  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
3765  * @class
3767  * @constructor
3768  * @param {string|jQuery} message Description of error
3769  * @param {Object} [config] Configuration options
3770  * @cfg {boolean} [recoverable=true] Error is recoverable.
3771  *  By default, errors are recoverable, and users can try the process again.
3772  * @cfg {boolean} [warning=false] Error is a warning.
3773  *  If the error is a warning, the error interface will include a
3774  *  'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
3775  *  is not triggered a second time if the user chooses to continue.
3776  */
3777 OO.ui.Error = function OoUiError( message, config ) {
3778         // Allow passing positional parameters inside the config object
3779         if ( OO.isPlainObject( message ) && config === undefined ) {
3780                 config = message;
3781                 message = config.message;
3782         }
3784         // Configuration initialization
3785         config = config || {};
3787         // Properties
3788         this.message = message instanceof jQuery ? message : String( message );
3789         this.recoverable = config.recoverable === undefined || !!config.recoverable;
3790         this.warning = !!config.warning;
3793 /* Setup */
3795 OO.initClass( OO.ui.Error );
3797 /* Methods */
3800  * Check if the error is recoverable.
3802  * If the error is recoverable, users are able to try the process again.
3804  * @return {boolean} Error is recoverable
3805  */
3806 OO.ui.Error.prototype.isRecoverable = function () {
3807         return this.recoverable;
3811  * Check if the error is a warning.
3813  * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
3815  * @return {boolean} Error is warning
3816  */
3817 OO.ui.Error.prototype.isWarning = function () {
3818         return this.warning;
3822  * Get error message as DOM nodes.
3824  * @return {jQuery} Error message in DOM nodes
3825  */
3826 OO.ui.Error.prototype.getMessage = function () {
3827         return this.message instanceof jQuery ?
3828                 this.message.clone() :
3829                 $( '<div>' ).text( this.message ).contents();
3833  * Get the error message text.
3835  * @return {string} Error message
3836  */
3837 OO.ui.Error.prototype.getMessageText = function () {
3838         return this.message instanceof jQuery ? this.message.text() : this.message;
3842  * Wraps an HTML snippet for use with configuration values which default
3843  * to strings.  This bypasses the default html-escaping done to string
3844  * values.
3846  * @class
3848  * @constructor
3849  * @param {string} [content] HTML content
3850  */
3851 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
3852         // Properties
3853         this.content = content;
3856 /* Setup */
3858 OO.initClass( OO.ui.HtmlSnippet );
3860 /* Methods */
3863  * Render into HTML.
3865  * @return {string} Unchanged HTML snippet.
3866  */
3867 OO.ui.HtmlSnippet.prototype.toString = function () {
3868         return this.content;
3872  * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
3873  * or a function:
3875  * - **number**: the process will wait for the specified number of milliseconds before proceeding.
3876  * - **promise**: the process will continue to the next step when the promise is successfully resolved
3877  *  or stop if the promise is rejected.
3878  * - **function**: the process will execute the function. The process will stop if the function returns
3879  *  either a boolean `false` or a promise that is rejected; if the function returns a number, the process
3880  *  will wait for that number of milliseconds before proceeding.
3882  * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
3883  * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
3884  * its remaining steps will not be performed.
3886  * @class
3888  * @constructor
3889  * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
3890  *  that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
3891  * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
3892  *  a number or promise.
3893  * @return {Object} Step object, with `callback` and `context` properties
3894  */
3895 OO.ui.Process = function ( step, context ) {
3896         // Properties
3897         this.steps = [];
3899         // Initialization
3900         if ( step !== undefined ) {
3901                 this.next( step, context );
3902         }
3905 /* Setup */
3907 OO.initClass( OO.ui.Process );
3909 /* Methods */
3912  * Start the process.
3914  * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
3915  *  If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
3916  *  and any remaining steps are not performed.
3917  */
3918 OO.ui.Process.prototype.execute = function () {
3919         var i, len, promise;
3921         /**
3922          * Continue execution.
3923          *
3924          * @ignore
3925          * @param {Array} step A function and the context it should be called in
3926          * @return {Function} Function that continues the process
3927          */
3928         function proceed( step ) {
3929                 return function () {
3930                         // Execute step in the correct context
3931                         var deferred,
3932                                 result = step.callback.call( step.context );
3934                         if ( result === false ) {
3935                                 // Use rejected promise for boolean false results
3936                                 return $.Deferred().reject( [] ).promise();
3937                         }
3938                         if ( typeof result === 'number' ) {
3939                                 if ( result < 0 ) {
3940                                         throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3941                                 }
3942                                 // Use a delayed promise for numbers, expecting them to be in milliseconds
3943                                 deferred = $.Deferred();
3944                                 setTimeout( deferred.resolve, result );
3945                                 return deferred.promise();
3946                         }
3947                         if ( result instanceof OO.ui.Error ) {
3948                                 // Use rejected promise for error
3949                                 return $.Deferred().reject( [ result ] ).promise();
3950                         }
3951                         if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3952                                 // Use rejected promise for list of errors
3953                                 return $.Deferred().reject( result ).promise();
3954                         }
3955                         // Duck-type the object to see if it can produce a promise
3956                         if ( result && $.isFunction( result.promise ) ) {
3957                                 // Use a promise generated from the result
3958                                 return result.promise();
3959                         }
3960                         // Use resolved promise for other results
3961                         return $.Deferred().resolve().promise();
3962                 };
3963         }
3965         if ( this.steps.length ) {
3966                 // Generate a chain reaction of promises
3967                 promise = proceed( this.steps[ 0 ] )();
3968                 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3969                         promise = promise.then( proceed( this.steps[ i ] ) );
3970                 }
3971         } else {
3972                 promise = $.Deferred().resolve().promise();
3973         }
3975         return promise;
3979  * Create a process step.
3981  * @private
3982  * @param {number|jQuery.Promise|Function} step
3984  * - Number of milliseconds to wait before proceeding
3985  * - Promise that must be resolved before proceeding
3986  * - Function to execute
3987  *   - If the function returns a boolean false the process will stop
3988  *   - If the function returns a promise, the process will continue to the next
3989  *     step when the promise is resolved or stop if the promise is rejected
3990  *   - If the function returns a number, the process will wait for that number of
3991  *     milliseconds before proceeding
3992  * @param {Object} [context=null] Execution context of the function. The context is
3993  *  ignored if the step is a number or promise.
3994  * @return {Object} Step object, with `callback` and `context` properties
3995  */
3996 OO.ui.Process.prototype.createStep = function ( step, context ) {
3997         if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3998                 return {
3999                         callback: function () {
4000                                 return step;
4001                         },
4002                         context: null
4003                 };
4004         }
4005         if ( $.isFunction( step ) ) {
4006                 return {
4007                         callback: step,
4008                         context: context
4009                 };
4010         }
4011         throw new Error( 'Cannot create process step: number, promise or function expected' );
4015  * Add step to the beginning of the process.
4017  * @inheritdoc #createStep
4018  * @return {OO.ui.Process} this
4019  * @chainable
4020  */
4021 OO.ui.Process.prototype.first = function ( step, context ) {
4022         this.steps.unshift( this.createStep( step, context ) );
4023         return this;
4027  * Add step to the end of the process.
4029  * @inheritdoc #createStep
4030  * @return {OO.ui.Process} this
4031  * @chainable
4032  */
4033 OO.ui.Process.prototype.next = function ( step, context ) {
4034         this.steps.push( this.createStep( step, context ) );
4035         return this;
4039  * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
4040  * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
4041  * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
4043  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
4045  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
4047  * @class
4048  * @extends OO.Factory
4049  * @constructor
4050  */
4051 OO.ui.ToolFactory = function OoUiToolFactory() {
4052         // Parent constructor
4053         OO.ui.ToolFactory.parent.call( this );
4056 /* Setup */
4058 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
4060 /* Methods */
4063  * Get tools from the factory
4065  * @param {Array} include Included tools
4066  * @param {Array} exclude Excluded tools
4067  * @param {Array} promote Promoted tools
4068  * @param {Array} demote Demoted tools
4069  * @return {string[]} List of tools
4070  */
4071 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
4072         var i, len, included, promoted, demoted,
4073                 auto = [],
4074                 used = {};
4076         // Collect included and not excluded tools
4077         included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
4079         // Promotion
4080         promoted = this.extract( promote, used );
4081         demoted = this.extract( demote, used );
4083         // Auto
4084         for ( i = 0, len = included.length; i < len; i++ ) {
4085                 if ( !used[ included[ i ] ] ) {
4086                         auto.push( included[ i ] );
4087                 }
4088         }
4090         return promoted.concat( auto ).concat( demoted );
4094  * Get a flat list of names from a list of names or groups.
4096  * Tools can be specified in the following ways:
4098  * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
4099  * - All tools in a group: `{ group: 'group-name' }`
4100  * - All tools: `'*'`
4102  * @private
4103  * @param {Array|string} collection List of tools
4104  * @param {Object} [used] Object with names that should be skipped as properties; extracted
4105  *  names will be added as properties
4106  * @return {string[]} List of extracted names
4107  */
4108 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
4109         var i, len, item, name, tool,
4110                 names = [];
4112         if ( collection === '*' ) {
4113                 for ( name in this.registry ) {
4114                         tool = this.registry[ name ];
4115                         if (
4116                                 // Only add tools by group name when auto-add is enabled
4117                                 tool.static.autoAddToCatchall &&
4118                                 // Exclude already used tools
4119                                 ( !used || !used[ name ] )
4120                         ) {
4121                                 names.push( name );
4122                                 if ( used ) {
4123                                         used[ name ] = true;
4124                                 }
4125                         }
4126                 }
4127         } else if ( Array.isArray( collection ) ) {
4128                 for ( i = 0, len = collection.length; i < len; i++ ) {
4129                         item = collection[ i ];
4130                         // Allow plain strings as shorthand for named tools
4131                         if ( typeof item === 'string' ) {
4132                                 item = { name: item };
4133                         }
4134                         if ( OO.isPlainObject( item ) ) {
4135                                 if ( item.group ) {
4136                                         for ( name in this.registry ) {
4137                                                 tool = this.registry[ name ];
4138                                                 if (
4139                                                         // Include tools with matching group
4140                                                         tool.static.group === item.group &&
4141                                                         // Only add tools by group name when auto-add is enabled
4142                                                         tool.static.autoAddToGroup &&
4143                                                         // Exclude already used tools
4144                                                         ( !used || !used[ name ] )
4145                                                 ) {
4146                                                         names.push( name );
4147                                                         if ( used ) {
4148                                                                 used[ name ] = true;
4149                                                         }
4150                                                 }
4151                                         }
4152                                 // Include tools with matching name and exclude already used tools
4153                                 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
4154                                         names.push( item.name );
4155                                         if ( used ) {
4156                                                 used[ item.name ] = true;
4157                                         }
4158                                 }
4159                         }
4160                 }
4161         }
4162         return names;
4166  * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
4167  * specify a symbolic name and be registered with the factory. The following classes are registered by
4168  * default:
4170  * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
4171  * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
4172  * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
4174  * See {@link OO.ui.Toolbar toolbars} for an example.
4176  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
4178  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
4179  * @class
4180  * @extends OO.Factory
4181  * @constructor
4182  */
4183 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
4184         var i, l, defaultClasses;
4185         // Parent constructor
4186         OO.Factory.call( this );
4188         defaultClasses = this.constructor.static.getDefaultClasses();
4190         // Register default toolgroups
4191         for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
4192                 this.register( defaultClasses[ i ] );
4193         }
4196 /* Setup */
4198 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
4200 /* Static Methods */
4203  * Get a default set of classes to be registered on construction.
4205  * @return {Function[]} Default classes
4206  */
4207 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
4208         return [
4209                 OO.ui.BarToolGroup,
4210                 OO.ui.ListToolGroup,
4211                 OO.ui.MenuToolGroup
4212         ];
4216  * Theme logic.
4218  * @abstract
4219  * @class
4221  * @constructor
4222  * @param {Object} [config] Configuration options
4223  */
4224 OO.ui.Theme = function OoUiTheme( config ) {
4225         // Configuration initialization
4226         config = config || {};
4229 /* Setup */
4231 OO.initClass( OO.ui.Theme );
4233 /* Methods */
4236  * Get a list of classes to be applied to a widget.
4238  * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
4239  * otherwise state transitions will not work properly.
4241  * @param {OO.ui.Element} element Element for which to get classes
4242  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4243  */
4244 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
4245         return { on: [], off: [] };
4249  * Update CSS classes provided by the theme.
4251  * For elements with theme logic hooks, this should be called any time there's a state change.
4253  * @param {OO.ui.Element} element Element for which to update classes
4254  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4255  */
4256 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
4257         var $elements = $( [] ),
4258                 classes = this.getElementClasses( element );
4260         if ( element.$icon ) {
4261                 $elements = $elements.add( element.$icon );
4262         }
4263         if ( element.$indicator ) {
4264                 $elements = $elements.add( element.$indicator );
4265         }
4267         $elements
4268                 .removeClass( classes.off.join( ' ' ) )
4269                 .addClass( classes.on.join( ' ' ) );
4273  * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
4274  * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
4275  * order in which users will navigate through the focusable elements via the "tab" key.
4277  *     @example
4278  *     // TabIndexedElement is mixed into the ButtonWidget class
4279  *     // to provide a tabIndex property.
4280  *     var button1 = new OO.ui.ButtonWidget( {
4281  *         label: 'fourth',
4282  *         tabIndex: 4
4283  *     } );
4284  *     var button2 = new OO.ui.ButtonWidget( {
4285  *         label: 'second',
4286  *         tabIndex: 2
4287  *     } );
4288  *     var button3 = new OO.ui.ButtonWidget( {
4289  *         label: 'third',
4290  *         tabIndex: 3
4291  *     } );
4292  *     var button4 = new OO.ui.ButtonWidget( {
4293  *         label: 'first',
4294  *         tabIndex: 1
4295  *     } );
4296  *     $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
4298  * @abstract
4299  * @class
4301  * @constructor
4302  * @param {Object} [config] Configuration options
4303  * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
4304  *  the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
4305  *  functionality will be applied to it instead.
4306  * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
4307  *  order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
4308  *  to remove the element from the tab-navigation flow.
4309  */
4310 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
4311         // Configuration initialization
4312         config = $.extend( { tabIndex: 0 }, config );
4314         // Properties
4315         this.$tabIndexed = null;
4316         this.tabIndex = null;
4318         // Events
4319         this.connect( this, { disable: 'onTabIndexedElementDisable' } );
4321         // Initialization
4322         this.setTabIndex( config.tabIndex );
4323         this.setTabIndexedElement( config.$tabIndexed || this.$element );
4326 /* Setup */
4328 OO.initClass( OO.ui.mixin.TabIndexedElement );
4330 /* Methods */
4333  * Set the element that should use the tabindex functionality.
4335  * This method is used to retarget a tabindex mixin so that its functionality applies
4336  * to the specified element. If an element is currently using the functionality, the mixin’s
4337  * effect on that element is removed before the new element is set up.
4339  * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
4340  * @chainable
4341  */
4342 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
4343         var tabIndex = this.tabIndex;
4344         // Remove attributes from old $tabIndexed
4345         this.setTabIndex( null );
4346         // Force update of new $tabIndexed
4347         this.$tabIndexed = $tabIndexed;
4348         this.tabIndex = tabIndex;
4349         return this.updateTabIndex();
4353  * Set the value of the tabindex.
4355  * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
4356  * @chainable
4357  */
4358 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
4359         tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
4361         if ( this.tabIndex !== tabIndex ) {
4362                 this.tabIndex = tabIndex;
4363                 this.updateTabIndex();
4364         }
4366         return this;
4370  * Update the `tabindex` attribute, in case of changes to tab index or
4371  * disabled state.
4373  * @private
4374  * @chainable
4375  */
4376 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
4377         if ( this.$tabIndexed ) {
4378                 if ( this.tabIndex !== null ) {
4379                         // Do not index over disabled elements
4380                         this.$tabIndexed.attr( {
4381                                 tabindex: this.isDisabled() ? -1 : this.tabIndex,
4382                                 // Support: ChromeVox and NVDA
4383                                 // These do not seem to inherit aria-disabled from parent elements
4384                                 'aria-disabled': this.isDisabled().toString()
4385                         } );
4386                 } else {
4387                         this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
4388                 }
4389         }
4390         return this;
4394  * Handle disable events.
4396  * @private
4397  * @param {boolean} disabled Element is disabled
4398  */
4399 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
4400         this.updateTabIndex();
4404  * Get the value of the tabindex.
4406  * @return {number|null} Tabindex value
4407  */
4408 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
4409         return this.tabIndex;
4413  * ButtonElement is often mixed into other classes to generate a button, which is a clickable
4414  * interface element that can be configured with access keys for accessibility.
4415  * See the [OOjs UI documentation on MediaWiki] [1] for examples.
4417  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
4418  * @abstract
4419  * @class
4421  * @constructor
4422  * @param {Object} [config] Configuration options
4423  * @cfg {jQuery} [$button] The button element created by the class.
4424  *  If this configuration is omitted, the button element will use a generated `<a>`.
4425  * @cfg {boolean} [framed=true] Render the button with a frame
4426  */
4427 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
4428         // Configuration initialization
4429         config = config || {};
4431         // Properties
4432         this.$button = null;
4433         this.framed = null;
4434         this.active = false;
4435         this.onMouseUpHandler = this.onMouseUp.bind( this );
4436         this.onMouseDownHandler = this.onMouseDown.bind( this );
4437         this.onKeyDownHandler = this.onKeyDown.bind( this );
4438         this.onKeyUpHandler = this.onKeyUp.bind( this );
4439         this.onClickHandler = this.onClick.bind( this );
4440         this.onKeyPressHandler = this.onKeyPress.bind( this );
4442         // Initialization
4443         this.$element.addClass( 'oo-ui-buttonElement' );
4444         this.toggleFramed( config.framed === undefined || config.framed );
4445         this.setButtonElement( config.$button || $( '<a>' ) );
4448 /* Setup */
4450 OO.initClass( OO.ui.mixin.ButtonElement );
4452 /* Static Properties */
4455  * Cancel mouse down events.
4457  * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
4458  * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
4459  * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
4460  * parent widget.
4462  * @static
4463  * @inheritable
4464  * @property {boolean}
4465  */
4466 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
4468 /* Events */
4471  * A 'click' event is emitted when the button element is clicked.
4473  * @event click
4474  */
4476 /* Methods */
4479  * Set the button element.
4481  * This method is used to retarget a button mixin so that its functionality applies to
4482  * the specified button element instead of the one created by the class. If a button element
4483  * is already set, the method will remove the mixin’s effect on that element.
4485  * @param {jQuery} $button Element to use as button
4486  */
4487 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
4488         if ( this.$button ) {
4489                 this.$button
4490                         .removeClass( 'oo-ui-buttonElement-button' )
4491                         .removeAttr( 'role accesskey' )
4492                         .off( {
4493                                 mousedown: this.onMouseDownHandler,
4494                                 keydown: this.onKeyDownHandler,
4495                                 click: this.onClickHandler,
4496                                 keypress: this.onKeyPressHandler
4497                         } );
4498         }
4500         this.$button = $button
4501                 .addClass( 'oo-ui-buttonElement-button' )
4502                 .attr( { role: 'button' } )
4503                 .on( {
4504                         mousedown: this.onMouseDownHandler,
4505                         keydown: this.onKeyDownHandler,
4506                         click: this.onClickHandler,
4507                         keypress: this.onKeyPressHandler
4508                 } );
4512  * Handles mouse down events.
4514  * @protected
4515  * @param {jQuery.Event} e Mouse down event
4516  */
4517 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
4518         if ( this.isDisabled() || e.which !== 1 ) {
4519                 return;
4520         }
4521         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4522         // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
4523         // reliably remove the pressed class
4524         OO.ui.addCaptureEventListener( this.getElementDocument(), 'mouseup', this.onMouseUpHandler );
4525         // Prevent change of focus unless specifically configured otherwise
4526         if ( this.constructor.static.cancelButtonMouseDownEvents ) {
4527                 return false;
4528         }
4532  * Handles mouse up events.
4534  * @protected
4535  * @param {jQuery.Event} e Mouse up event
4536  */
4537 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
4538         if ( this.isDisabled() || e.which !== 1 ) {
4539                 return;
4540         }
4541         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4542         // Stop listening for mouseup, since we only needed this once
4543         OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup', this.onMouseUpHandler );
4547  * Handles mouse click events.
4549  * @protected
4550  * @param {jQuery.Event} e Mouse click event
4551  * @fires click
4552  */
4553 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
4554         if ( !this.isDisabled() && e.which === 1 ) {
4555                 if ( this.emit( 'click' ) ) {
4556                         return false;
4557                 }
4558         }
4562  * Handles key down events.
4564  * @protected
4565  * @param {jQuery.Event} e Key down event
4566  */
4567 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
4568         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4569                 return;
4570         }
4571         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4572         // Run the keyup handler no matter where the key is when the button is let go, so we can
4573         // reliably remove the pressed class
4574         OO.ui.addCaptureEventListener( this.getElementDocument(), 'keyup', this.onKeyUpHandler );
4578  * Handles key up events.
4580  * @protected
4581  * @param {jQuery.Event} e Key up event
4582  */
4583 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
4584         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4585                 return;
4586         }
4587         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4588         // Stop listening for keyup, since we only needed this once
4589         OO.ui.removeCaptureEventListener( this.getElementDocument(), 'keyup', this.onKeyUpHandler );
4593  * Handles key press events.
4595  * @protected
4596  * @param {jQuery.Event} e Key press event
4597  * @fires click
4598  */
4599 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
4600         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
4601                 if ( this.emit( 'click' ) ) {
4602                         return false;
4603                 }
4604         }
4608  * Check if button has a frame.
4610  * @return {boolean} Button is framed
4611  */
4612 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
4613         return this.framed;
4617  * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
4619  * @param {boolean} [framed] Make button framed, omit to toggle
4620  * @chainable
4621  */
4622 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
4623         framed = framed === undefined ? !this.framed : !!framed;
4624         if ( framed !== this.framed ) {
4625                 this.framed = framed;
4626                 this.$element
4627                         .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
4628                         .toggleClass( 'oo-ui-buttonElement-framed', framed );
4629                 this.updateThemeClasses();
4630         }
4632         return this;
4636  * Set the button to its 'active' state.
4638  * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
4639  * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
4640  * for other button types.
4642  * @param {boolean} [value] Make button active
4643  * @chainable
4644  */
4645 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
4646         this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
4647         return this;
4651  * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4652  * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4653  * items from the group is done through the interface the class provides.
4654  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4656  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4658  * @abstract
4659  * @class
4661  * @constructor
4662  * @param {Object} [config] Configuration options
4663  * @cfg {jQuery} [$group] The container element created by the class. If this configuration
4664  *  is omitted, the group element will use a generated `<div>`.
4665  */
4666 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
4667         // Configuration initialization
4668         config = config || {};
4670         // Properties
4671         this.$group = null;
4672         this.items = [];
4673         this.aggregateItemEvents = {};
4675         // Initialization
4676         this.setGroupElement( config.$group || $( '<div>' ) );
4679 /* Methods */
4682  * Set the group element.
4684  * If an element is already set, items will be moved to the new element.
4686  * @param {jQuery} $group Element to use as group
4687  */
4688 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
4689         var i, len;
4691         this.$group = $group;
4692         for ( i = 0, len = this.items.length; i < len; i++ ) {
4693                 this.$group.append( this.items[ i ].$element );
4694         }
4698  * Check if a group contains no items.
4700  * @return {boolean} Group is empty
4701  */
4702 OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
4703         return !this.items.length;
4707  * Get all items in the group.
4709  * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
4710  * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
4711  * from a group).
4713  * @return {OO.ui.Element[]} An array of items.
4714  */
4715 OO.ui.mixin.GroupElement.prototype.getItems = function () {
4716         return this.items.slice( 0 );
4720  * Get an item by its data.
4722  * Only the first item with matching data will be returned. To return all matching items,
4723  * use the #getItemsFromData method.
4725  * @param {Object} data Item data to search for
4726  * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4727  */
4728 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
4729         var i, len, item,
4730                 hash = OO.getHash( data );
4732         for ( i = 0, len = this.items.length; i < len; i++ ) {
4733                 item = this.items[ i ];
4734                 if ( hash === OO.getHash( item.getData() ) ) {
4735                         return item;
4736                 }
4737         }
4739         return null;
4743  * Get items by their data.
4745  * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
4747  * @param {Object} data Item data to search for
4748  * @return {OO.ui.Element[]} Items with equivalent data
4749  */
4750 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
4751         var i, len, item,
4752                 hash = OO.getHash( data ),
4753                 items = [];
4755         for ( i = 0, len = this.items.length; i < len; i++ ) {
4756                 item = this.items[ i ];
4757                 if ( hash === OO.getHash( item.getData() ) ) {
4758                         items.push( item );
4759                 }
4760         }
4762         return items;
4766  * Aggregate the events emitted by the group.
4768  * When events are aggregated, the group will listen to all contained items for the event,
4769  * and then emit the event under a new name. The new event will contain an additional leading
4770  * parameter containing the item that emitted the original event. Other arguments emitted from
4771  * the original event are passed through.
4773  * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
4774  *  aggregated  (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
4775  *  A `null` value will remove aggregated events.
4777  * @throws {Error} An error is thrown if aggregation already exists.
4778  */
4779 OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
4780         var i, len, item, add, remove, itemEvent, groupEvent;
4782         for ( itemEvent in events ) {
4783                 groupEvent = events[ itemEvent ];
4785                 // Remove existing aggregated event
4786                 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4787                         // Don't allow duplicate aggregations
4788                         if ( groupEvent ) {
4789                                 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
4790                         }
4791                         // Remove event aggregation from existing items
4792                         for ( i = 0, len = this.items.length; i < len; i++ ) {
4793                                 item = this.items[ i ];
4794                                 if ( item.connect && item.disconnect ) {
4795                                         remove = {};
4796                                         remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4797                                         item.disconnect( this, remove );
4798                                 }
4799                         }
4800                         // Prevent future items from aggregating event
4801                         delete this.aggregateItemEvents[ itemEvent ];
4802                 }
4804                 // Add new aggregate event
4805                 if ( groupEvent ) {
4806                         // Make future items aggregate event
4807                         this.aggregateItemEvents[ itemEvent ] = groupEvent;
4808                         // Add event aggregation to existing items
4809                         for ( i = 0, len = this.items.length; i < len; i++ ) {
4810                                 item = this.items[ i ];
4811                                 if ( item.connect && item.disconnect ) {
4812                                         add = {};
4813                                         add[ itemEvent ] = [ 'emit', groupEvent, item ];
4814                                         item.connect( this, add );
4815                                 }
4816                         }
4817                 }
4818         }
4822  * Add items to the group.
4824  * Items will be added to the end of the group array unless the optional `index` parameter specifies
4825  * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
4827  * @param {OO.ui.Element[]} items An array of items to add to the group
4828  * @param {number} [index] Index of the insertion point
4829  * @chainable
4830  */
4831 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
4832         var i, len, item, event, events, currentIndex,
4833                 itemElements = [];
4835         for ( i = 0, len = items.length; i < len; i++ ) {
4836                 item = items[ i ];
4838                 // Check if item exists then remove it first, effectively "moving" it
4839                 currentIndex = this.items.indexOf( item );
4840                 if ( currentIndex >= 0 ) {
4841                         this.removeItems( [ item ] );
4842                         // Adjust index to compensate for removal
4843                         if ( currentIndex < index ) {
4844                                 index--;
4845                         }
4846                 }
4847                 // Add the item
4848                 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4849                         events = {};
4850                         for ( event in this.aggregateItemEvents ) {
4851                                 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4852                         }
4853                         item.connect( this, events );
4854                 }
4855                 item.setElementGroup( this );
4856                 itemElements.push( item.$element.get( 0 ) );
4857         }
4859         if ( index === undefined || index < 0 || index >= this.items.length ) {
4860                 this.$group.append( itemElements );
4861                 this.items.push.apply( this.items, items );
4862         } else if ( index === 0 ) {
4863                 this.$group.prepend( itemElements );
4864                 this.items.unshift.apply( this.items, items );
4865         } else {
4866                 this.items[ index ].$element.before( itemElements );
4867                 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4868         }
4870         return this;
4874  * Remove the specified items from a group.
4876  * Removed items are detached (not removed) from the DOM so that they may be reused.
4877  * To remove all items from a group, you may wish to use the #clearItems method instead.
4879  * @param {OO.ui.Element[]} items An array of items to remove
4880  * @chainable
4881  */
4882 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
4883         var i, len, item, index, remove, itemEvent;
4885         // Remove specific items
4886         for ( i = 0, len = items.length; i < len; i++ ) {
4887                 item = items[ i ];
4888                 index = this.items.indexOf( item );
4889                 if ( index !== -1 ) {
4890                         if (
4891                                 item.connect && item.disconnect &&
4892                                 !$.isEmptyObject( this.aggregateItemEvents )
4893                         ) {
4894                                 remove = {};
4895                                 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4896                                         remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4897                                 }
4898                                 item.disconnect( this, remove );
4899                         }
4900                         item.setElementGroup( null );
4901                         this.items.splice( index, 1 );
4902                         item.$element.detach();
4903                 }
4904         }
4906         return this;
4910  * Clear all items from the group.
4912  * Cleared items are detached from the DOM, not removed, so that they may be reused.
4913  * To remove only a subset of items from a group, use the #removeItems method.
4915  * @chainable
4916  */
4917 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
4918         var i, len, item, remove, itemEvent;
4920         // Remove all items
4921         for ( i = 0, len = this.items.length; i < len; i++ ) {
4922                 item = this.items[ i ];
4923                 if (
4924                         item.connect && item.disconnect &&
4925                         !$.isEmptyObject( this.aggregateItemEvents )
4926                 ) {
4927                         remove = {};
4928                         if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4929                                 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4930                         }
4931                         item.disconnect( this, remove );
4932                 }
4933                 item.setElementGroup( null );
4934                 item.$element.detach();
4935         }
4937         this.items = [];
4938         return this;
4942  * DraggableElement is a mixin class used to create elements that can be clicked
4943  * and dragged by a mouse to a new position within a group. This class must be used
4944  * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
4945  * the draggable elements.
4947  * @abstract
4948  * @class
4950  * @constructor
4951  */
4952 OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement() {
4953         // Properties
4954         this.index = null;
4956         // Initialize and events
4957         this.$element
4958                 .attr( 'draggable', true )
4959                 .addClass( 'oo-ui-draggableElement' )
4960                 .on( {
4961                         dragstart: this.onDragStart.bind( this ),
4962                         dragover: this.onDragOver.bind( this ),
4963                         dragend: this.onDragEnd.bind( this ),
4964                         drop: this.onDrop.bind( this )
4965                 } );
4968 OO.initClass( OO.ui.mixin.DraggableElement );
4970 /* Events */
4973  * @event dragstart
4975  * A dragstart event is emitted when the user clicks and begins dragging an item.
4976  * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4977  */
4980  * @event dragend
4981  * A dragend event is emitted when the user drags an item and releases the mouse,
4982  * thus terminating the drag operation.
4983  */
4986  * @event drop
4987  * A drop event is emitted when the user drags an item and then releases the mouse button
4988  * over a valid target.
4989  */
4991 /* Static Properties */
4994  * @inheritdoc OO.ui.mixin.ButtonElement
4995  */
4996 OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
4998 /* Methods */
5001  * Respond to dragstart event.
5003  * @private
5004  * @param {jQuery.Event} event jQuery event
5005  * @fires dragstart
5006  */
5007 OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
5008         var dataTransfer = e.originalEvent.dataTransfer;
5009         // Define drop effect
5010         dataTransfer.dropEffect = 'none';
5011         dataTransfer.effectAllowed = 'move';
5012         // Support: Firefox
5013         // We must set up a dataTransfer data property or Firefox seems to
5014         // ignore the fact the element is draggable.
5015         try {
5016                 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
5017         } catch ( err ) {
5018                 // The above is only for Firefox. Move on if it fails.
5019         }
5020         // Add dragging class
5021         this.$element.addClass( 'oo-ui-draggableElement-dragging' );
5022         // Emit event
5023         this.emit( 'dragstart', this );
5024         return true;
5028  * Respond to dragend event.
5030  * @private
5031  * @fires dragend
5032  */
5033 OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
5034         this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
5035         this.emit( 'dragend' );
5039  * Handle drop event.
5041  * @private
5042  * @param {jQuery.Event} event jQuery event
5043  * @fires drop
5044  */
5045 OO.ui.mixin.DraggableElement.prototype.onDrop = function ( e ) {
5046         e.preventDefault();
5047         this.emit( 'drop', e );
5051  * In order for drag/drop to work, the dragover event must
5052  * return false and stop propogation.
5054  * @private
5055  */
5056 OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
5057         e.preventDefault();
5061  * Set item index.
5062  * Store it in the DOM so we can access from the widget drag event
5064  * @private
5065  * @param {number} Item index
5066  */
5067 OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
5068         if ( this.index !== index ) {
5069                 this.index = index;
5070                 this.$element.data( 'index', index );
5071         }
5075  * Get item index
5077  * @private
5078  * @return {number} Item index
5079  */
5080 OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
5081         return this.index;
5085  * DraggableGroupElement is a mixin class used to create a group element to
5086  * contain draggable elements, which are items that can be clicked and dragged by a mouse.
5087  * The class is used with OO.ui.mixin.DraggableElement.
5089  * @abstract
5090  * @class
5091  * @mixins OO.ui.mixin.GroupElement
5093  * @constructor
5094  * @param {Object} [config] Configuration options
5095  * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
5096  *  should match the layout of the items. Items displayed in a single row
5097  *  or in several rows should use horizontal orientation. The vertical orientation should only be
5098  *  used when the items are displayed in a single column. Defaults to 'vertical'
5099  */
5100 OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
5101         // Configuration initialization
5102         config = config || {};
5104         // Parent constructor
5105         OO.ui.mixin.GroupElement.call( this, config );
5107         // Properties
5108         this.orientation = config.orientation || 'vertical';
5109         this.dragItem = null;
5110         this.itemDragOver = null;
5111         this.itemKeys = {};
5112         this.sideInsertion = '';
5114         // Events
5115         this.aggregate( {
5116                 dragstart: 'itemDragStart',
5117                 dragend: 'itemDragEnd',
5118                 drop: 'itemDrop'
5119         } );
5120         this.connect( this, {
5121                 itemDragStart: 'onItemDragStart',
5122                 itemDrop: 'onItemDrop',
5123                 itemDragEnd: 'onItemDragEnd'
5124         } );
5125         this.$element.on( {
5126                 dragover: this.onDragOver.bind( this ),
5127                 dragleave: this.onDragLeave.bind( this )
5128         } );
5130         // Initialize
5131         if ( Array.isArray( config.items ) ) {
5132                 this.addItems( config.items );
5133         }
5134         this.$placeholder = $( '<div>' )
5135                 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
5136         this.$element
5137                 .addClass( 'oo-ui-draggableGroupElement' )
5138                 .append( this.$status )
5139                 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
5140                 .prepend( this.$placeholder );
5143 /* Setup */
5144 OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
5146 /* Events */
5149  * A 'reorder' event is emitted when the order of items in the group changes.
5151  * @event reorder
5152  * @param {OO.ui.mixin.DraggableElement} item Reordered item
5153  * @param {number} [newIndex] New index for the item
5154  */
5156 /* Methods */
5159  * Respond to item drag start event
5161  * @private
5162  * @param {OO.ui.mixin.DraggableElement} item Dragged item
5163  */
5164 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
5165         var i, len;
5167         // Map the index of each object
5168         for ( i = 0, len = this.items.length; i < len; i++ ) {
5169                 this.items[ i ].setIndex( i );
5170         }
5172         if ( this.orientation === 'horizontal' ) {
5173                 // Set the height of the indicator
5174                 this.$placeholder.css( {
5175                         height: item.$element.outerHeight(),
5176                         width: 2
5177                 } );
5178         } else {
5179                 // Set the width of the indicator
5180                 this.$placeholder.css( {
5181                         height: 2,
5182                         width: item.$element.outerWidth()
5183                 } );
5184         }
5185         this.setDragItem( item );
5189  * Respond to item drag end event
5191  * @private
5192  */
5193 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragEnd = function () {
5194         this.unsetDragItem();
5195         return false;
5199  * Handle drop event and switch the order of the items accordingly
5201  * @private
5202  * @param {OO.ui.mixin.DraggableElement} item Dropped item
5203  * @fires reorder
5204  */
5205 OO.ui.mixin.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
5206         var toIndex = item.getIndex();
5207         // Check if the dropped item is from the current group
5208         // TODO: Figure out a way to configure a list of legally droppable
5209         // elements even if they are not yet in the list
5210         if ( this.getDragItem() ) {
5211                 // If the insertion point is 'after', the insertion index
5212                 // is shifted to the right (or to the left in RTL, hence 'after')
5213                 if ( this.sideInsertion === 'after' ) {
5214                         toIndex++;
5215                 }
5216                 // Emit change event
5217                 this.emit( 'reorder', this.getDragItem(), toIndex );
5218         }
5219         this.unsetDragItem();
5220         // Return false to prevent propogation
5221         return false;
5225  * Handle dragleave event.
5227  * @private
5228  */
5229 OO.ui.mixin.DraggableGroupElement.prototype.onDragLeave = function () {
5230         // This means the item was dragged outside the widget
5231         this.$placeholder
5232                 .css( 'left', 0 )
5233                 .addClass( 'oo-ui-element-hidden' );
5237  * Respond to dragover event
5239  * @private
5240  * @param {jQuery.Event} event Event details
5241  */
5242 OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
5243         var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
5244                 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
5245                 clientX = e.originalEvent.clientX,
5246                 clientY = e.originalEvent.clientY;
5248         // Get the OptionWidget item we are dragging over
5249         dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
5250         $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
5251         if ( $optionWidget[ 0 ] ) {
5252                 itemOffset = $optionWidget.offset();
5253                 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
5254                 itemPosition = $optionWidget.position();
5255                 itemIndex = $optionWidget.data( 'index' );
5256         }
5258         if (
5259                 itemOffset &&
5260                 this.isDragging() &&
5261                 itemIndex !== this.getDragItem().getIndex()
5262         ) {
5263                 if ( this.orientation === 'horizontal' ) {
5264                         // Calculate where the mouse is relative to the item width
5265                         itemSize = itemBoundingRect.width;
5266                         itemMidpoint = itemBoundingRect.left + itemSize / 2;
5267                         dragPosition = clientX;
5268                         // Which side of the item we hover over will dictate
5269                         // where the placeholder will appear, on the left or
5270                         // on the right
5271                         cssOutput = {
5272                                 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
5273                                 top: itemPosition.top
5274                         };
5275                 } else {
5276                         // Calculate where the mouse is relative to the item height
5277                         itemSize = itemBoundingRect.height;
5278                         itemMidpoint = itemBoundingRect.top + itemSize / 2;
5279                         dragPosition = clientY;
5280                         // Which side of the item we hover over will dictate
5281                         // where the placeholder will appear, on the top or
5282                         // on the bottom
5283                         cssOutput = {
5284                                 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
5285                                 left: itemPosition.left
5286                         };
5287                 }
5288                 // Store whether we are before or after an item to rearrange
5289                 // For horizontal layout, we need to account for RTL, as this is flipped
5290                 if (  this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
5291                         this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
5292                 } else {
5293                         this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
5294                 }
5295                 // Add drop indicator between objects
5296                 this.$placeholder
5297                         .css( cssOutput )
5298                         .removeClass( 'oo-ui-element-hidden' );
5299         } else {
5300                 // This means the item was dragged outside the widget
5301                 this.$placeholder
5302                         .css( 'left', 0 )
5303                         .addClass( 'oo-ui-element-hidden' );
5304         }
5305         // Prevent default
5306         e.preventDefault();
5310  * Set a dragged item
5312  * @param {OO.ui.mixin.DraggableElement} item Dragged item
5313  */
5314 OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
5315         this.dragItem = item;
5319  * Unset the current dragged item
5320  */
5321 OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
5322         this.dragItem = null;
5323         this.itemDragOver = null;
5324         this.$placeholder.addClass( 'oo-ui-element-hidden' );
5325         this.sideInsertion = '';
5329  * Get the item that is currently being dragged.
5331  * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
5332  */
5333 OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
5334         return this.dragItem;
5338  * Check if an item in the group is currently being dragged.
5340  * @return {Boolean} Item is being dragged
5341  */
5342 OO.ui.mixin.DraggableGroupElement.prototype.isDragging = function () {
5343         return this.getDragItem() !== null;
5347  * IconElement is often mixed into other classes to generate an icon.
5348  * Icons are graphics, about the size of normal text. They are used to aid the user
5349  * in locating a control or to convey information in a space-efficient way. See the
5350  * [OOjs UI documentation on MediaWiki] [1] for a list of icons
5351  * included in the library.
5353  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5355  * @abstract
5356  * @class
5358  * @constructor
5359  * @param {Object} [config] Configuration options
5360  * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
5361  *  the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
5362  *  the icon element be set to an existing icon instead of the one generated by this class, set a
5363  *  value using a jQuery selection. For example:
5365  *      // Use a <div> tag instead of a <span>
5366  *     $icon: $("<div>")
5367  *     // Use an existing icon element instead of the one generated by the class
5368  *     $icon: this.$element
5369  *     // Use an icon element from a child widget
5370  *     $icon: this.childwidget.$element
5371  * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
5372  *  symbolic names.  A map is used for i18n purposes and contains a `default` icon
5373  *  name and additional names keyed by language code. The `default` name is used when no icon is keyed
5374  *  by the user's language.
5376  *  Example of an i18n map:
5378  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5379  *  See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
5380  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5381  * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
5382  *  text. The icon title is displayed when users move the mouse over the icon.
5383  */
5384 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
5385         // Configuration initialization
5386         config = config || {};
5388         // Properties
5389         this.$icon = null;
5390         this.icon = null;
5391         this.iconTitle = null;
5393         // Initialization
5394         this.setIcon( config.icon || this.constructor.static.icon );
5395         this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
5396         this.setIconElement( config.$icon || $( '<span>' ) );
5399 /* Setup */
5401 OO.initClass( OO.ui.mixin.IconElement );
5403 /* Static Properties */
5406  * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
5407  * for i18n purposes and contains a `default` icon name and additional names keyed by
5408  * language code. The `default` name is used when no icon is keyed by the user's language.
5410  * Example of an i18n map:
5412  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5414  * Note: the static property will be overridden if the #icon configuration is used.
5416  * @static
5417  * @inheritable
5418  * @property {Object|string}
5419  */
5420 OO.ui.mixin.IconElement.static.icon = null;
5423  * The icon title, displayed when users move the mouse over the icon. The value can be text, a
5424  * function that returns title text, or `null` for no title.
5426  * The static property will be overridden if the #iconTitle configuration is used.
5428  * @static
5429  * @inheritable
5430  * @property {string|Function|null}
5431  */
5432 OO.ui.mixin.IconElement.static.iconTitle = null;
5434 /* Methods */
5437  * Set the icon element. This method is used to retarget an icon mixin so that its functionality
5438  * applies to the specified icon element instead of the one created by the class. If an icon
5439  * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
5440  * and mixin methods will no longer affect the element.
5442  * @param {jQuery} $icon Element to use as icon
5443  */
5444 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
5445         if ( this.$icon ) {
5446                 this.$icon
5447                         .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
5448                         .removeAttr( 'title' );
5449         }
5451         this.$icon = $icon
5452                 .addClass( 'oo-ui-iconElement-icon' )
5453                 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
5454         if ( this.iconTitle !== null ) {
5455                 this.$icon.attr( 'title', this.iconTitle );
5456         }
5458         this.updateThemeClasses();
5462  * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
5463  * The icon parameter can also be set to a map of icon names. See the #icon config setting
5464  * for an example.
5466  * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
5467  *  by language code, or `null` to remove the icon.
5468  * @chainable
5469  */
5470 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
5471         icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
5472         icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
5474         if ( this.icon !== icon ) {
5475                 if ( this.$icon ) {
5476                         if ( this.icon !== null ) {
5477                                 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
5478                         }
5479                         if ( icon !== null ) {
5480                                 this.$icon.addClass( 'oo-ui-icon-' + icon );
5481                         }
5482                 }
5483                 this.icon = icon;
5484         }
5486         this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
5487         this.updateThemeClasses();
5489         return this;
5493  * Set the icon title. Use `null` to remove the title.
5495  * @param {string|Function|null} iconTitle A text string used as the icon title,
5496  *  a function that returns title text, or `null` for no title.
5497  * @chainable
5498  */
5499 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
5500         iconTitle = typeof iconTitle === 'function' ||
5501                 ( typeof iconTitle === 'string' && iconTitle.length ) ?
5502                         OO.ui.resolveMsg( iconTitle ) : null;
5504         if ( this.iconTitle !== iconTitle ) {
5505                 this.iconTitle = iconTitle;
5506                 if ( this.$icon ) {
5507                         if ( this.iconTitle !== null ) {
5508                                 this.$icon.attr( 'title', iconTitle );
5509                         } else {
5510                                 this.$icon.removeAttr( 'title' );
5511                         }
5512                 }
5513         }
5515         return this;
5519  * Get the symbolic name of the icon.
5521  * @return {string} Icon name
5522  */
5523 OO.ui.mixin.IconElement.prototype.getIcon = function () {
5524         return this.icon;
5528  * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
5530  * @return {string} Icon title text
5531  */
5532 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
5533         return this.iconTitle;
5537  * IndicatorElement is often mixed into other classes to generate an indicator.
5538  * Indicators are small graphics that are generally used in two ways:
5540  * - To draw attention to the status of an item. For example, an indicator might be
5541  *   used to show that an item in a list has errors that need to be resolved.
5542  * - To clarify the function of a control that acts in an exceptional way (a button
5543  *   that opens a menu instead of performing an action directly, for example).
5545  * For a list of indicators included in the library, please see the
5546  * [OOjs UI documentation on MediaWiki] [1].
5548  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5550  * @abstract
5551  * @class
5553  * @constructor
5554  * @param {Object} [config] Configuration options
5555  * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5556  *  configuration is omitted, the indicator element will use a generated `<span>`.
5557  * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
5558  *  See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5559  *  in the library.
5560  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5561  * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5562  *  or a function that returns title text. The indicator title is displayed when users move
5563  *  the mouse over the indicator.
5564  */
5565 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
5566         // Configuration initialization
5567         config = config || {};
5569         // Properties
5570         this.$indicator = null;
5571         this.indicator = null;
5572         this.indicatorTitle = null;
5574         // Initialization
5575         this.setIndicator( config.indicator || this.constructor.static.indicator );
5576         this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
5577         this.setIndicatorElement( config.$indicator || $( '<span>' ) );
5580 /* Setup */
5582 OO.initClass( OO.ui.mixin.IndicatorElement );
5584 /* Static Properties */
5587  * Symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
5588  * The static property will be overridden if the #indicator configuration is used.
5590  * @static
5591  * @inheritable
5592  * @property {string|null}
5593  */
5594 OO.ui.mixin.IndicatorElement.static.indicator = null;
5597  * A text string used as the indicator title, a function that returns title text, or `null`
5598  * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5600  * @static
5601  * @inheritable
5602  * @property {string|Function|null}
5603  */
5604 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
5606 /* Methods */
5609  * Set the indicator element.
5611  * If an element is already set, it will be cleaned up before setting up the new element.
5613  * @param {jQuery} $indicator Element to use as indicator
5614  */
5615 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
5616         if ( this.$indicator ) {
5617                 this.$indicator
5618                         .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
5619                         .removeAttr( 'title' );
5620         }
5622         this.$indicator = $indicator
5623                 .addClass( 'oo-ui-indicatorElement-indicator' )
5624                 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
5625         if ( this.indicatorTitle !== null ) {
5626                 this.$indicator.attr( 'title', this.indicatorTitle );
5627         }
5629         this.updateThemeClasses();
5633  * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
5635  * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
5636  * @chainable
5637  */
5638 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
5639         indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
5641         if ( this.indicator !== indicator ) {
5642                 if ( this.$indicator ) {
5643                         if ( this.indicator !== null ) {
5644                                 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
5645                         }
5646                         if ( indicator !== null ) {
5647                                 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
5648                         }
5649                 }
5650                 this.indicator = indicator;
5651         }
5653         this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
5654         this.updateThemeClasses();
5656         return this;
5660  * Set the indicator title.
5662  * The title is displayed when a user moves the mouse over the indicator.
5664  * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
5665  *   `null` for no indicator title
5666  * @chainable
5667  */
5668 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
5669         indicatorTitle = typeof indicatorTitle === 'function' ||
5670                 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
5671                         OO.ui.resolveMsg( indicatorTitle ) : null;
5673         if ( this.indicatorTitle !== indicatorTitle ) {
5674                 this.indicatorTitle = indicatorTitle;
5675                 if ( this.$indicator ) {
5676                         if ( this.indicatorTitle !== null ) {
5677                                 this.$indicator.attr( 'title', indicatorTitle );
5678                         } else {
5679                                 this.$indicator.removeAttr( 'title' );
5680                         }
5681                 }
5682         }
5684         return this;
5688  * Get the symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
5690  * @return {string} Symbolic name of indicator
5691  */
5692 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
5693         return this.indicator;
5697  * Get the indicator title.
5699  * The title is displayed when a user moves the mouse over the indicator.
5701  * @return {string} Indicator title text
5702  */
5703 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
5704         return this.indicatorTitle;
5708  * LabelElement is often mixed into other classes to generate a label, which
5709  * helps identify the function of an interface element.
5710  * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5712  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5714  * @abstract
5715  * @class
5717  * @constructor
5718  * @param {Object} [config] Configuration options
5719  * @cfg {jQuery} [$label] The label element created by the class. If this
5720  *  configuration is omitted, the label element will use a generated `<span>`.
5721  * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
5722  *  as a plaintext string, a jQuery selection of elements, or a function that will produce a string
5723  *  in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
5724  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5725  * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5726  *  The label will be truncated to fit if necessary.
5727  */
5728 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
5729         // Configuration initialization
5730         config = config || {};
5732         // Properties
5733         this.$label = null;
5734         this.label = null;
5735         this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
5737         // Initialization
5738         this.setLabel( config.label || this.constructor.static.label );
5739         this.setLabelElement( config.$label || $( '<span>' ) );
5742 /* Setup */
5744 OO.initClass( OO.ui.mixin.LabelElement );
5746 /* Events */
5749  * @event labelChange
5750  * @param {string} value
5751  */
5753 /* Static Properties */
5756  * The label text. The label can be specified as a plaintext string, a function that will
5757  * produce a string in the future, or `null` for no label. The static value will
5758  * be overridden if a label is specified with the #label config option.
5760  * @static
5761  * @inheritable
5762  * @property {string|Function|null}
5763  */
5764 OO.ui.mixin.LabelElement.static.label = null;
5766 /* Methods */
5769  * Set the label element.
5771  * If an element is already set, it will be cleaned up before setting up the new element.
5773  * @param {jQuery} $label Element to use as label
5774  */
5775 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
5776         if ( this.$label ) {
5777                 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
5778         }
5780         this.$label = $label.addClass( 'oo-ui-labelElement-label' );
5781         this.setLabelContent( this.label );
5785  * Set the label.
5787  * An empty string will result in the label being hidden. A string containing only whitespace will
5788  * be converted to a single `&nbsp;`.
5790  * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5791  *  text; or null for no label
5792  * @chainable
5793  */
5794 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
5795         label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
5796         label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
5798         this.$element.toggleClass( 'oo-ui-labelElement', !!label );
5800         if ( this.label !== label ) {
5801                 if ( this.$label ) {
5802                         this.setLabelContent( label );
5803                 }
5804                 this.label = label;
5805                 this.emit( 'labelChange' );
5806         }
5808         return this;
5812  * Get the label.
5814  * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5815  *  text; or null for no label
5816  */
5817 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
5818         return this.label;
5822  * Fit the label.
5824  * @chainable
5825  */
5826 OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
5827         if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
5828                 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
5829         }
5831         return this;
5835  * Set the content of the label.
5837  * Do not call this method until after the label element has been set by #setLabelElement.
5839  * @private
5840  * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5841  *  text; or null for no label
5842  */
5843 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
5844         if ( typeof label === 'string' ) {
5845                 if ( label.match( /^\s*$/ ) ) {
5846                         // Convert whitespace only string to a single non-breaking space
5847                         this.$label.html( '&nbsp;' );
5848                 } else {
5849                         this.$label.text( label );
5850                 }
5851         } else if ( label instanceof OO.ui.HtmlSnippet ) {
5852                 this.$label.html( label.toString() );
5853         } else if ( label instanceof jQuery ) {
5854                 this.$label.empty().append( label );
5855         } else {
5856                 this.$label.empty();
5857         }
5861  * LookupElement is a mixin that creates a {@link OO.ui.FloatingMenuSelectWidget menu} of suggested values for
5862  * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
5863  * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
5864  * from the lookup menu, that value becomes the value of the input field.
5866  * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
5867  * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
5868  * re-enable lookups.
5870  * See the [OOjs UI demos][1] for an example.
5872  * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
5874  * @class
5875  * @abstract
5877  * @constructor
5878  * @param {Object} [config] Configuration options
5879  * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
5880  * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
5881  * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
5882  *  By default, the lookup menu is not generated and displayed until the user begins to type.
5883  */
5884 OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
5885         // Configuration initialization
5886         config = config || {};
5888         // Properties
5889         this.$overlay = config.$overlay || this.$element;
5890         this.lookupMenu = new OO.ui.FloatingMenuSelectWidget( {
5891                 widget: this,
5892                 input: this,
5893                 $container: config.$container || this.$element
5894         } );
5896         this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
5898         this.lookupCache = {};
5899         this.lookupQuery = null;
5900         this.lookupRequest = null;
5901         this.lookupsDisabled = false;
5902         this.lookupInputFocused = false;
5904         // Events
5905         this.$input.on( {
5906                 focus: this.onLookupInputFocus.bind( this ),
5907                 blur: this.onLookupInputBlur.bind( this ),
5908                 mousedown: this.onLookupInputMouseDown.bind( this )
5909         } );
5910         this.connect( this, { change: 'onLookupInputChange' } );
5911         this.lookupMenu.connect( this, {
5912                 toggle: 'onLookupMenuToggle',
5913                 choose: 'onLookupMenuItemChoose'
5914         } );
5916         // Initialization
5917         this.$element.addClass( 'oo-ui-lookupElement' );
5918         this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5919         this.$overlay.append( this.lookupMenu.$element );
5922 /* Methods */
5925  * Handle input focus event.
5927  * @protected
5928  * @param {jQuery.Event} e Input focus event
5929  */
5930 OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
5931         this.lookupInputFocused = true;
5932         this.populateLookupMenu();
5936  * Handle input blur event.
5938  * @protected
5939  * @param {jQuery.Event} e Input blur event
5940  */
5941 OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
5942         this.closeLookupMenu();
5943         this.lookupInputFocused = false;
5947  * Handle input mouse down event.
5949  * @protected
5950  * @param {jQuery.Event} e Input mouse down event
5951  */
5952 OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
5953         // Only open the menu if the input was already focused.
5954         // This way we allow the user to open the menu again after closing it with Esc
5955         // by clicking in the input. Opening (and populating) the menu when initially
5956         // clicking into the input is handled by the focus handler.
5957         if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5958                 this.populateLookupMenu();
5959         }
5963  * Handle input change event.
5965  * @protected
5966  * @param {string} value New input value
5967  */
5968 OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
5969         if ( this.lookupInputFocused ) {
5970                 this.populateLookupMenu();
5971         }
5975  * Handle the lookup menu being shown/hidden.
5977  * @protected
5978  * @param {boolean} visible Whether the lookup menu is now visible.
5979  */
5980 OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5981         if ( !visible ) {
5982                 // When the menu is hidden, abort any active request and clear the menu.
5983                 // This has to be done here in addition to closeLookupMenu(), because
5984                 // MenuSelectWidget will close itself when the user presses Esc.
5985                 this.abortLookupRequest();
5986                 this.lookupMenu.clearItems();
5987         }
5991  * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5993  * @protected
5994  * @param {OO.ui.MenuOptionWidget} item Selected item
5995  */
5996 OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5997         this.setValue( item.getData() );
6001  * Get lookup menu.
6003  * @private
6004  * @return {OO.ui.FloatingMenuSelectWidget}
6005  */
6006 OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
6007         return this.lookupMenu;
6011  * Disable or re-enable lookups.
6013  * When lookups are disabled, calls to #populateLookupMenu will be ignored.
6015  * @param {boolean} disabled Disable lookups
6016  */
6017 OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
6018         this.lookupsDisabled = !!disabled;
6022  * Open the menu. If there are no entries in the menu, this does nothing.
6024  * @private
6025  * @chainable
6026  */
6027 OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
6028         if ( !this.lookupMenu.isEmpty() ) {
6029                 this.lookupMenu.toggle( true );
6030         }
6031         return this;
6035  * Close the menu, empty it, and abort any pending request.
6037  * @private
6038  * @chainable
6039  */
6040 OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
6041         this.lookupMenu.toggle( false );
6042         this.abortLookupRequest();
6043         this.lookupMenu.clearItems();
6044         return this;
6048  * Request menu items based on the input's current value, and when they arrive,
6049  * populate the menu with these items and show the menu.
6051  * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
6053  * @private
6054  * @chainable
6055  */
6056 OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
6057         var widget = this,
6058                 value = this.getValue();
6060         if ( this.lookupsDisabled || this.isReadOnly() ) {
6061                 return;
6062         }
6064         // If the input is empty, clear the menu, unless suggestions when empty are allowed.
6065         if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
6066                 this.closeLookupMenu();
6067         // Skip population if there is already a request pending for the current value
6068         } else if ( value !== this.lookupQuery ) {
6069                 this.getLookupMenuItems()
6070                         .done( function ( items ) {
6071                                 widget.lookupMenu.clearItems();
6072                                 if ( items.length ) {
6073                                         widget.lookupMenu
6074                                                 .addItems( items )
6075                                                 .toggle( true );
6076                                         widget.initializeLookupMenuSelection();
6077                                 } else {
6078                                         widget.lookupMenu.toggle( false );
6079                                 }
6080                         } )
6081                         .fail( function () {
6082                                 widget.lookupMenu.clearItems();
6083                         } );
6084         }
6086         return this;
6090  * Highlight the first selectable item in the menu.
6092  * @private
6093  * @chainable
6094  */
6095 OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
6096         if ( !this.lookupMenu.getSelectedItem() ) {
6097                 this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
6098         }
6102  * Get lookup menu items for the current query.
6104  * @private
6105  * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
6106  *   the done event. If the request was aborted to make way for a subsequent request, this promise
6107  *   will not be rejected: it will remain pending forever.
6108  */
6109 OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
6110         var widget = this,
6111                 value = this.getValue(),
6112                 deferred = $.Deferred(),
6113                 ourRequest;
6115         this.abortLookupRequest();
6116         if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
6117                 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
6118         } else {
6119                 this.pushPending();
6120                 this.lookupQuery = value;
6121                 ourRequest = this.lookupRequest = this.getLookupRequest();
6122                 ourRequest
6123                         .always( function () {
6124                                 // We need to pop pending even if this is an old request, otherwise
6125                                 // the widget will remain pending forever.
6126                                 // TODO: this assumes that an aborted request will fail or succeed soon after
6127                                 // being aborted, or at least eventually. It would be nice if we could popPending()
6128                                 // at abort time, but only if we knew that we hadn't already called popPending()
6129                                 // for that request.
6130                                 widget.popPending();
6131                         } )
6132                         .done( function ( response ) {
6133                                 // If this is an old request (and aborting it somehow caused it to still succeed),
6134                                 // ignore its success completely
6135                                 if ( ourRequest === widget.lookupRequest ) {
6136                                         widget.lookupQuery = null;
6137                                         widget.lookupRequest = null;
6138                                         widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( response );
6139                                         deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
6140                                 }
6141                         } )
6142                         .fail( function () {
6143                                 // If this is an old request (or a request failing because it's being aborted),
6144                                 // ignore its failure completely
6145                                 if ( ourRequest === widget.lookupRequest ) {
6146                                         widget.lookupQuery = null;
6147                                         widget.lookupRequest = null;
6148                                         deferred.reject();
6149                                 }
6150                         } );
6151         }
6152         return deferred.promise();
6156  * Abort the currently pending lookup request, if any.
6158  * @private
6159  */
6160 OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
6161         var oldRequest = this.lookupRequest;
6162         if ( oldRequest ) {
6163                 // First unset this.lookupRequest to the fail handler will notice
6164                 // that the request is no longer current
6165                 this.lookupRequest = null;
6166                 this.lookupQuery = null;
6167                 oldRequest.abort();
6168         }
6172  * Get a new request object of the current lookup query value.
6174  * @protected
6175  * @abstract
6176  * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
6177  */
6178 OO.ui.mixin.LookupElement.prototype.getLookupRequest = function () {
6179         // Stub, implemented in subclass
6180         return null;
6184  * Pre-process data returned by the request from #getLookupRequest.
6186  * The return value of this function will be cached, and any further queries for the given value
6187  * will use the cache rather than doing API requests.
6189  * @protected
6190  * @abstract
6191  * @param {Mixed} response Response from server
6192  * @return {Mixed} Cached result data
6193  */
6194 OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
6195         // Stub, implemented in subclass
6196         return [];
6200  * Get a list of menu option widgets from the (possibly cached) data returned by
6201  * #getLookupCacheDataFromResponse.
6203  * @protected
6204  * @abstract
6205  * @param {Mixed} data Cached result data, usually an array
6206  * @return {OO.ui.MenuOptionWidget[]} Menu items
6207  */
6208 OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
6209         // Stub, implemented in subclass
6210         return [];
6214  * Set the read-only state of the widget.
6216  * This will also disable/enable the lookups functionality.
6218  * @param {boolean} readOnly Make input read-only
6219  * @chainable
6220  */
6221 OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
6222         // Parent method
6223         // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
6224         OO.ui.TextInputWidget.prototype.setReadOnly.call( this, readOnly );
6226         // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
6227         if ( this.isReadOnly() && this.lookupMenu ) {
6228                 this.closeLookupMenu();
6229         }
6231         return this;
6235  * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6236  * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6237  * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6238  * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6240  * @abstract
6241  * @class
6243  * @constructor
6244  * @param {Object} [config] Configuration options
6245  * @cfg {Object} [popup] Configuration to pass to popup
6246  * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6247  */
6248 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
6249         // Configuration initialization
6250         config = config || {};
6252         // Properties
6253         this.popup = new OO.ui.PopupWidget( $.extend(
6254                 { autoClose: true },
6255                 config.popup,
6256                 { $autoCloseIgnore: this.$element }
6257         ) );
6260 /* Methods */
6263  * Get popup.
6265  * @return {OO.ui.PopupWidget} Popup widget
6266  */
6267 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
6268         return this.popup;
6272  * The FlaggedElement class is an attribute mixin, meaning that it is used to add
6273  * additional functionality to an element created by another class. The class provides
6274  * a ‘flags’ property assigned the name (or an array of names) of styling flags,
6275  * which are used to customize the look and feel of a widget to better describe its
6276  * importance and functionality.
6278  * The library currently contains the following styling flags for general use:
6280  * - **progressive**:  Progressive styling is applied to convey that the widget will move the user forward in a process.
6281  * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
6282  * - **constructive**: Constructive styling is applied to convey that the widget will create something.
6284  * The flags affect the appearance of the buttons:
6286  *     @example
6287  *     // FlaggedElement is mixed into ButtonWidget to provide styling flags
6288  *     var button1 = new OO.ui.ButtonWidget( {
6289  *         label: 'Constructive',
6290  *         flags: 'constructive'
6291  *     } );
6292  *     var button2 = new OO.ui.ButtonWidget( {
6293  *         label: 'Destructive',
6294  *         flags: 'destructive'
6295  *     } );
6296  *     var button3 = new OO.ui.ButtonWidget( {
6297  *         label: 'Progressive',
6298  *         flags: 'progressive'
6299  *     } );
6300  *     $( 'body' ).append( button1.$element, button2.$element, button3.$element );
6302  * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
6303  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6305  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6307  * @abstract
6308  * @class
6310  * @constructor
6311  * @param {Object} [config] Configuration options
6312  * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
6313  *  Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
6314  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6315  * @cfg {jQuery} [$flagged] The flagged element. By default,
6316  *  the flagged functionality is applied to the element created by the class ($element).
6317  *  If a different element is specified, the flagged functionality will be applied to it instead.
6318  */
6319 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
6320         // Configuration initialization
6321         config = config || {};
6323         // Properties
6324         this.flags = {};
6325         this.$flagged = null;
6327         // Initialization
6328         this.setFlags( config.flags );
6329         this.setFlaggedElement( config.$flagged || this.$element );
6332 /* Events */
6335  * @event flag
6336  * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
6337  * parameter contains the name of each modified flag and indicates whether it was
6338  * added or removed.
6340  * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
6341  * that the flag was added, `false` that the flag was removed.
6342  */
6344 /* Methods */
6347  * Set the flagged element.
6349  * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
6350  * If an element is already set, the method will remove the mixin’s effect on that element.
6352  * @param {jQuery} $flagged Element that should be flagged
6353  */
6354 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
6355         var classNames = Object.keys( this.flags ).map( function ( flag ) {
6356                 return 'oo-ui-flaggedElement-' + flag;
6357         } ).join( ' ' );
6359         if ( this.$flagged ) {
6360                 this.$flagged.removeClass( classNames );
6361         }
6363         this.$flagged = $flagged.addClass( classNames );
6367  * Check if the specified flag is set.
6369  * @param {string} flag Name of flag
6370  * @return {boolean} The flag is set
6371  */
6372 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
6373         // This may be called before the constructor, thus before this.flags is set
6374         return this.flags && ( flag in this.flags );
6378  * Get the names of all flags set.
6380  * @return {string[]} Flag names
6381  */
6382 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
6383         // This may be called before the constructor, thus before this.flags is set
6384         return Object.keys( this.flags || {} );
6388  * Clear all flags.
6390  * @chainable
6391  * @fires flag
6392  */
6393 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
6394         var flag, className,
6395                 changes = {},
6396                 remove = [],
6397                 classPrefix = 'oo-ui-flaggedElement-';
6399         for ( flag in this.flags ) {
6400                 className = classPrefix + flag;
6401                 changes[ flag ] = false;
6402                 delete this.flags[ flag ];
6403                 remove.push( className );
6404         }
6406         if ( this.$flagged ) {
6407                 this.$flagged.removeClass( remove.join( ' ' ) );
6408         }
6410         this.updateThemeClasses();
6411         this.emit( 'flag', changes );
6413         return this;
6417  * Add one or more flags.
6419  * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
6420  *  or an object keyed by flag name with a boolean value that indicates whether the flag should
6421  *  be added (`true`) or removed (`false`).
6422  * @chainable
6423  * @fires flag
6424  */
6425 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
6426         var i, len, flag, className,
6427                 changes = {},
6428                 add = [],
6429                 remove = [],
6430                 classPrefix = 'oo-ui-flaggedElement-';
6432         if ( typeof flags === 'string' ) {
6433                 className = classPrefix + flags;
6434                 // Set
6435                 if ( !this.flags[ flags ] ) {
6436                         this.flags[ flags ] = true;
6437                         add.push( className );
6438                 }
6439         } else if ( Array.isArray( flags ) ) {
6440                 for ( i = 0, len = flags.length; i < len; i++ ) {
6441                         flag = flags[ i ];
6442                         className = classPrefix + flag;
6443                         // Set
6444                         if ( !this.flags[ flag ] ) {
6445                                 changes[ flag ] = true;
6446                                 this.flags[ flag ] = true;
6447                                 add.push( className );
6448                         }
6449                 }
6450         } else if ( OO.isPlainObject( flags ) ) {
6451                 for ( flag in flags ) {
6452                         className = classPrefix + flag;
6453                         if ( flags[ flag ] ) {
6454                                 // Set
6455                                 if ( !this.flags[ flag ] ) {
6456                                         changes[ flag ] = true;
6457                                         this.flags[ flag ] = true;
6458                                         add.push( className );
6459                                 }
6460                         } else {
6461                                 // Remove
6462                                 if ( this.flags[ flag ] ) {
6463                                         changes[ flag ] = false;
6464                                         delete this.flags[ flag ];
6465                                         remove.push( className );
6466                                 }
6467                         }
6468                 }
6469         }
6471         if ( this.$flagged ) {
6472                 this.$flagged
6473                         .addClass( add.join( ' ' ) )
6474                         .removeClass( remove.join( ' ' ) );
6475         }
6477         this.updateThemeClasses();
6478         this.emit( 'flag', changes );
6480         return this;
6484  * TitledElement is mixed into other classes to provide a `title` attribute.
6485  * Titles are rendered by the browser and are made visible when the user moves
6486  * the mouse over the element. Titles are not visible on touch devices.
6488  *     @example
6489  *     // TitledElement provides a 'title' attribute to the
6490  *     // ButtonWidget class
6491  *     var button = new OO.ui.ButtonWidget( {
6492  *         label: 'Button with Title',
6493  *         title: 'I am a button'
6494  *     } );
6495  *     $( 'body' ).append( button.$element );
6497  * @abstract
6498  * @class
6500  * @constructor
6501  * @param {Object} [config] Configuration options
6502  * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
6503  *  If this config is omitted, the title functionality is applied to $element, the
6504  *  element created by the class.
6505  * @cfg {string|Function} [title] The title text or a function that returns text. If
6506  *  this config is omitted, the value of the {@link #static-title static title} property is used.
6507  */
6508 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
6509         // Configuration initialization
6510         config = config || {};
6512         // Properties
6513         this.$titled = null;
6514         this.title = null;
6516         // Initialization
6517         this.setTitle( config.title || this.constructor.static.title );
6518         this.setTitledElement( config.$titled || this.$element );
6521 /* Setup */
6523 OO.initClass( OO.ui.mixin.TitledElement );
6525 /* Static Properties */
6528  * The title text, a function that returns text, or `null` for no title. The value of the static property
6529  * is overridden if the #title config option is used.
6531  * @static
6532  * @inheritable
6533  * @property {string|Function|null}
6534  */
6535 OO.ui.mixin.TitledElement.static.title = null;
6537 /* Methods */
6540  * Set the titled element.
6542  * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
6543  * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
6545  * @param {jQuery} $titled Element that should use the 'titled' functionality
6546  */
6547 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
6548         if ( this.$titled ) {
6549                 this.$titled.removeAttr( 'title' );
6550         }
6552         this.$titled = $titled;
6553         if ( this.title ) {
6554                 this.$titled.attr( 'title', this.title );
6555         }
6559  * Set title.
6561  * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
6562  * @chainable
6563  */
6564 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
6565         title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
6567         if ( this.title !== title ) {
6568                 if ( this.$titled ) {
6569                         if ( title !== null ) {
6570                                 this.$titled.attr( 'title', title );
6571                         } else {
6572                                 this.$titled.removeAttr( 'title' );
6573                         }
6574                 }
6575                 this.title = title;
6576         }
6578         return this;
6582  * Get title.
6584  * @return {string} Title string
6585  */
6586 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
6587         return this.title;
6591  * Element that can be automatically clipped to visible boundaries.
6593  * Whenever the element's natural height changes, you have to call
6594  * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
6595  * clipping correctly.
6597  * The dimensions of #$clippableContainer will be compared to the boundaries of the
6598  * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
6599  * then #$clippable will be given a fixed reduced height and/or width and will be made
6600  * scrollable. By default, #$clippable and #$clippableContainer are the same element,
6601  * but you can build a static footer by setting #$clippableContainer to an element that contains
6602  * #$clippable and the footer.
6604  * @abstract
6605  * @class
6607  * @constructor
6608  * @param {Object} [config] Configuration options
6609  * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
6610  * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
6611  *   omit to use #$clippable
6612  */
6613 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
6614         // Configuration initialization
6615         config = config || {};
6617         // Properties
6618         this.$clippable = null;
6619         this.$clippableContainer = null;
6620         this.clipping = false;
6621         this.clippedHorizontally = false;
6622         this.clippedVertically = false;
6623         this.$clippableScrollableContainer = null;
6624         this.$clippableScroller = null;
6625         this.$clippableWindow = null;
6626         this.idealWidth = null;
6627         this.idealHeight = null;
6628         this.onClippableScrollHandler = this.clip.bind( this );
6629         this.onClippableWindowResizeHandler = this.clip.bind( this );
6631         // Initialization
6632         if ( config.$clippableContainer ) {
6633                 this.setClippableContainer( config.$clippableContainer );
6634         }
6635         this.setClippableElement( config.$clippable || this.$element );
6638 /* Methods */
6641  * Set clippable element.
6643  * If an element is already set, it will be cleaned up before setting up the new element.
6645  * @param {jQuery} $clippable Element to make clippable
6646  */
6647 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
6648         if ( this.$clippable ) {
6649                 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
6650                 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6651                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6652         }
6654         this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
6655         this.clip();
6659  * Set clippable container.
6661  * This is the container that will be measured when deciding whether to clip. When clipping,
6662  * #$clippable will be resized in order to keep the clippable container fully visible.
6664  * If the clippable container is unset, #$clippable will be used.
6666  * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
6667  */
6668 OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
6669         this.$clippableContainer = $clippableContainer;
6670         if ( this.$clippable ) {
6671                 this.clip();
6672         }
6676  * Toggle clipping.
6678  * Do not turn clipping on until after the element is attached to the DOM and visible.
6680  * @param {boolean} [clipping] Enable clipping, omit to toggle
6681  * @chainable
6682  */
6683 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
6684         clipping = clipping === undefined ? !this.clipping : !!clipping;
6686         if ( this.clipping !== clipping ) {
6687                 this.clipping = clipping;
6688                 if ( clipping ) {
6689                         this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
6690                         // If the clippable container is the root, we have to listen to scroll events and check
6691                         // jQuery.scrollTop on the window because of browser inconsistencies
6692                         this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
6693                                 $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
6694                                 this.$clippableScrollableContainer;
6695                         this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
6696                         this.$clippableWindow = $( this.getElementWindow() )
6697                                 .on( 'resize', this.onClippableWindowResizeHandler );
6698                         // Initial clip after visible
6699                         this.clip();
6700                 } else {
6701                         this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6702                         OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6704                         this.$clippableScrollableContainer = null;
6705                         this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
6706                         this.$clippableScroller = null;
6707                         this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
6708                         this.$clippableWindow = null;
6709                 }
6710         }
6712         return this;
6716  * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6718  * @return {boolean} Element will be clipped to the visible area
6719  */
6720 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
6721         return this.clipping;
6725  * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6727  * @return {boolean} Part of the element is being clipped
6728  */
6729 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
6730         return this.clippedHorizontally || this.clippedVertically;
6734  * Check if the right of the element is being clipped by the nearest scrollable container.
6736  * @return {boolean} Part of the element is being clipped
6737  */
6738 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
6739         return this.clippedHorizontally;
6743  * Check if the bottom of the element is being clipped by the nearest scrollable container.
6745  * @return {boolean} Part of the element is being clipped
6746  */
6747 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
6748         return this.clippedVertically;
6752  * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6754  * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6755  * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6756  */
6757 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
6758         this.idealWidth = width;
6759         this.idealHeight = height;
6761         if ( !this.clipping ) {
6762                 // Update dimensions
6763                 this.$clippable.css( { width: width, height: height } );
6764         }
6765         // While clipping, idealWidth and idealHeight are not considered
6769  * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6770  * the element's natural height changes.
6772  * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6773  * overlapped by, the visible area of the nearest scrollable container.
6775  * @chainable
6776  */
6777 OO.ui.mixin.ClippableElement.prototype.clip = function () {
6778         var $container, extraHeight, extraWidth, ccOffset,
6779                 $scrollableContainer, scOffset, scHeight, scWidth,
6780                 ccWidth, scrollerIsWindow, scrollTop, scrollLeft,
6781                 desiredWidth, desiredHeight, allotedWidth, allotedHeight,
6782                 naturalWidth, naturalHeight, clipWidth, clipHeight,
6783                 buffer = 7; // Chosen by fair dice roll
6785         if ( !this.clipping ) {
6786                 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
6787                 return this;
6788         }
6790         $container = this.$clippableContainer || this.$clippable;
6791         extraHeight = $container.outerHeight() - this.$clippable.outerHeight();
6792         extraWidth = $container.outerWidth() - this.$clippable.outerWidth();
6793         ccOffset = $container.offset();
6794         $scrollableContainer = this.$clippableScrollableContainer.is( 'html, body' ) ?
6795                 this.$clippableWindow : this.$clippableScrollableContainer;
6796         scOffset = $scrollableContainer.offset() || { top: 0, left: 0 };
6797         scHeight = $scrollableContainer.innerHeight() - buffer;
6798         scWidth = $scrollableContainer.innerWidth() - buffer;
6799         ccWidth = $container.outerWidth() + buffer;
6800         scrollerIsWindow = this.$clippableScroller[ 0 ] === this.$clippableWindow[ 0 ];
6801         scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0;
6802         scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0;
6803         desiredWidth = ccOffset.left < 0 ?
6804                 ccWidth + ccOffset.left :
6805                 ( scOffset.left + scrollLeft + scWidth ) - ccOffset.left;
6806         desiredHeight = ( scOffset.top + scrollTop + scHeight ) - ccOffset.top;
6807         allotedWidth = desiredWidth - extraWidth;
6808         allotedHeight = desiredHeight - extraHeight;
6809         naturalWidth = this.$clippable.prop( 'scrollWidth' );
6810         naturalHeight = this.$clippable.prop( 'scrollHeight' );
6811         clipWidth = allotedWidth < naturalWidth;
6812         clipHeight = allotedHeight < naturalHeight;
6814         if ( clipWidth ) {
6815                 this.$clippable.css( { overflowX: 'scroll', width: Math.max( 0, allotedWidth ) } );
6816         } else {
6817                 this.$clippable.css( { width: this.idealWidth ? this.idealWidth - extraWidth : '', overflowX: '' } );
6818         }
6819         if ( clipHeight ) {
6820                 this.$clippable.css( { overflowY: 'scroll', height: Math.max( 0, allotedHeight ) } );
6821         } else {
6822                 this.$clippable.css( { height: this.idealHeight ? this.idealHeight - extraHeight : '', overflowY: '' } );
6823         }
6825         // If we stopped clipping in at least one of the dimensions
6826         if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
6827                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6828         }
6830         this.clippedHorizontally = clipWidth;
6831         this.clippedVertically = clipHeight;
6833         return this;
6837  * Element that will stick under a specified container, even when it is inserted elsewhere in the
6838  * document (for example, in a OO.ui.Window's $overlay).
6840  * The elements's position is automatically calculated and maintained when window is resized or the
6841  * page is scrolled. If you reposition the container manually, you have to call #position to make
6842  * sure the element is still placed correctly.
6844  * As positioning is only possible when both the element and the container are attached to the DOM
6845  * and visible, it's only done after you call #togglePositioning. You might want to do this inside
6846  * the #toggle method to display a floating popup, for example.
6848  * @abstract
6849  * @class
6851  * @constructor
6852  * @param {Object} [config] Configuration options
6853  * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
6854  * @cfg {jQuery} [$floatableContainer] Node to position below
6855  */
6856 OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
6857         // Configuration initialization
6858         config = config || {};
6860         // Properties
6861         this.$floatable = null;
6862         this.$floatableContainer = null;
6863         this.$floatableWindow = null;
6864         this.$floatableClosestScrollable = null;
6865         this.onFloatableScrollHandler = this.position.bind( this );
6866         this.onFloatableWindowResizeHandler = this.position.bind( this );
6868         // Initialization
6869         this.setFloatableContainer( config.$floatableContainer );
6870         this.setFloatableElement( config.$floatable || this.$element );
6873 /* Methods */
6876  * Set floatable element.
6878  * If an element is already set, it will be cleaned up before setting up the new element.
6880  * @param {jQuery} $floatable Element to make floatable
6881  */
6882 OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
6883         if ( this.$floatable ) {
6884                 this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
6885                 this.$floatable.css( { left: '', top: '' } );
6886         }
6888         this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
6889         this.position();
6893  * Set floatable container.
6895  * The element will be always positioned under the specified container.
6897  * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
6898  */
6899 OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
6900         this.$floatableContainer = $floatableContainer;
6901         if ( this.$floatable ) {
6902                 this.position();
6903         }
6907  * Toggle positioning.
6909  * Do not turn positioning on until after the element is attached to the DOM and visible.
6911  * @param {boolean} [positioning] Enable positioning, omit to toggle
6912  * @chainable
6913  */
6914 OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
6915         var closestScrollableOfContainer, closestScrollableOfFloatable;
6917         positioning = positioning === undefined ? !this.positioning : !!positioning;
6919         if ( this.positioning !== positioning ) {
6920                 this.positioning = positioning;
6922                 closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
6923                 closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
6924                 if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
6925                         // If the scrollable is the root, we have to listen to scroll events
6926                         // on the window because of browser inconsistencies (or do we? someone should verify this)
6927                         if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
6928                                 closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
6929                         }
6930                 }
6932                 if ( positioning ) {
6933                         this.$floatableWindow = $( this.getElementWindow() );
6934                         this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
6936                         if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
6937                                 this.$floatableClosestScrollable = $( closestScrollableOfContainer );
6938                                 this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
6939                         }
6941                         // Initial position after visible
6942                         this.position();
6943                 } else {
6944                         if ( this.$floatableWindow ) {
6945                                 this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
6946                                 this.$floatableWindow = null;
6947                         }
6949                         if ( this.$floatableClosestScrollable ) {
6950                                 this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
6951                                 this.$floatableClosestScrollable = null;
6952                         }
6954                         this.$floatable.css( { left: '', top: '' } );
6955                 }
6956         }
6958         return this;
6962  * Position the floatable below its container.
6964  * This should only be done when both of them are attached to the DOM and visible.
6966  * @chainable
6967  */
6968 OO.ui.mixin.FloatableElement.prototype.position = function () {
6969         var pos;
6971         if ( !this.positioning ) {
6972                 return this;
6973         }
6975         pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
6977         // Position under container
6978         pos.top += this.$floatableContainer.height();
6979         this.$floatable.css( pos );
6981         // We updated the position, so re-evaluate the clipping state.
6982         // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
6983         // will not notice the need to update itself.)
6984         // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
6985         // it not listen to the right events in the right places?
6986         if ( this.clip ) {
6987                 this.clip();
6988         }
6990         return this;
6994  * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
6995  * Accesskeys allow an user to go to a specific element by using
6996  * a shortcut combination of a browser specific keys + the key
6997  * set to the field.
6999  *     @example
7000  *     // AccessKeyedElement provides an 'accesskey' attribute to the
7001  *     // ButtonWidget class
7002  *     var button = new OO.ui.ButtonWidget( {
7003  *         label: 'Button with Accesskey',
7004  *         accessKey: 'k'
7005  *     } );
7006  *     $( 'body' ).append( button.$element );
7008  * @abstract
7009  * @class
7011  * @constructor
7012  * @param {Object} [config] Configuration options
7013  * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
7014  *  If this config is omitted, the accesskey functionality is applied to $element, the
7015  *  element created by the class.
7016  * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
7017  *  this config is omitted, no accesskey will be added.
7018  */
7019 OO.ui.mixin.AccessKeyedElement = function OoUiMixinAccessKeyedElement( config ) {
7020         // Configuration initialization
7021         config = config || {};
7023         // Properties
7024         this.$accessKeyed = null;
7025         this.accessKey = null;
7027         // Initialization
7028         this.setAccessKey( config.accessKey || null );
7029         this.setAccessKeyedElement( config.$accessKeyed || this.$element );
7032 /* Setup */
7034 OO.initClass( OO.ui.mixin.AccessKeyedElement );
7036 /* Static Properties */
7039  * The access key, a function that returns a key, or `null` for no accesskey.
7041  * @static
7042  * @inheritable
7043  * @property {string|Function|null}
7044  */
7045 OO.ui.mixin.AccessKeyedElement.static.accessKey = null;
7047 /* Methods */
7050  * Set the accesskeyed element.
7052  * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
7053  * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
7055  * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
7056  */
7057 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKeyedElement = function ( $accessKeyed ) {
7058         if ( this.$accessKeyed ) {
7059                 this.$accessKeyed.removeAttr( 'accesskey' );
7060         }
7062         this.$accessKeyed = $accessKeyed;
7063         if ( this.accessKey ) {
7064                 this.$accessKeyed.attr( 'accesskey', this.accessKey );
7065         }
7069  * Set accesskey.
7071  * @param {string|Function|null} accesskey Key, a function that returns a key, or `null` for no accesskey
7072  * @chainable
7073  */
7074 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKey = function ( accessKey ) {
7075         accessKey = typeof accessKey === 'string' ? OO.ui.resolveMsg( accessKey ) : null;
7077         if ( this.accessKey !== accessKey ) {
7078                 if ( this.$accessKeyed ) {
7079                         if ( accessKey !== null ) {
7080                                 this.$accessKeyed.attr( 'accesskey', accessKey );
7081                         } else {
7082                                 this.$accessKeyed.removeAttr( 'accesskey' );
7083                         }
7084                 }
7085                 this.accessKey = accessKey;
7086         }
7088         return this;
7092  * Get accesskey.
7094  * @return {string} accessKey string
7095  */
7096 OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
7097         return this.accessKey;
7101  * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
7102  * Each tool is configured with a static name, title, and icon and is customized with the command to carry
7103  * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
7104  * which creates the tools on demand.
7106  * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
7107  * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
7108  * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
7110  * For more information, please see the [OOjs UI documentation on MediaWiki][1].
7111  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
7113  * @abstract
7114  * @class
7115  * @extends OO.ui.Widget
7116  * @mixins OO.ui.mixin.IconElement
7117  * @mixins OO.ui.mixin.FlaggedElement
7118  * @mixins OO.ui.mixin.TabIndexedElement
7120  * @constructor
7121  * @param {OO.ui.ToolGroup} toolGroup
7122  * @param {Object} [config] Configuration options
7123  * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
7124  *  the {@link #static-title static title} property is used.
7126  *  The title is used in different ways depending on the type of toolgroup that contains the tool. The
7127  *  title is used as a tooltip if the tool is part of a {@link OO.ui.BarToolGroup bar} toolgroup, or as the label text if the tool is
7128  *  part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
7130  *  For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
7131  *  is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
7132  *  To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
7133  */
7134 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
7135         // Allow passing positional parameters inside the config object
7136         if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
7137                 config = toolGroup;
7138                 toolGroup = config.toolGroup;
7139         }
7141         // Configuration initialization
7142         config = config || {};
7144         // Parent constructor
7145         OO.ui.Tool.parent.call( this, config );
7147         // Properties
7148         this.toolGroup = toolGroup;
7149         this.toolbar = this.toolGroup.getToolbar();
7150         this.active = false;
7151         this.$title = $( '<span>' );
7152         this.$accel = $( '<span>' );
7153         this.$link = $( '<a>' );
7154         this.title = null;
7156         // Mixin constructors
7157         OO.ui.mixin.IconElement.call( this, config );
7158         OO.ui.mixin.FlaggedElement.call( this, config );
7159         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
7161         // Events
7162         this.toolbar.connect( this, { updateState: 'onUpdateState' } );
7164         // Initialization
7165         this.$title.addClass( 'oo-ui-tool-title' );
7166         this.$accel
7167                 .addClass( 'oo-ui-tool-accel' )
7168                 .prop( {
7169                         // This may need to be changed if the key names are ever localized,
7170                         // but for now they are essentially written in English
7171                         dir: 'ltr',
7172                         lang: 'en'
7173                 } );
7174         this.$link
7175                 .addClass( 'oo-ui-tool-link' )
7176                 .append( this.$icon, this.$title, this.$accel )
7177                 .attr( 'role', 'button' );
7178         this.$element
7179                 .data( 'oo-ui-tool', this )
7180                 .addClass(
7181                         'oo-ui-tool ' + 'oo-ui-tool-name-' +
7182                         this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
7183                 )
7184                 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
7185                 .append( this.$link );
7186         this.setTitle( config.title || this.constructor.static.title );
7189 /* Setup */
7191 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
7192 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
7193 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
7194 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
7196 /* Static Properties */
7199  * @static
7200  * @inheritdoc
7201  */
7202 OO.ui.Tool.static.tagName = 'span';
7205  * Symbolic name of tool.
7207  * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
7208  * also be used when adding tools to toolgroups.
7210  * @abstract
7211  * @static
7212  * @inheritable
7213  * @property {string}
7214  */
7215 OO.ui.Tool.static.name = '';
7218  * Symbolic name of the group.
7220  * The group name is used to associate tools with each other so that they can be selected later by
7221  * a {@link OO.ui.ToolGroup toolgroup}.
7223  * @abstract
7224  * @static
7225  * @inheritable
7226  * @property {string}
7227  */
7228 OO.ui.Tool.static.group = '';
7231  * Tool title text or a function that returns title text. The value of the static property is overridden if the #title config option is used.
7233  * @abstract
7234  * @static
7235  * @inheritable
7236  * @property {string|Function}
7237  */
7238 OO.ui.Tool.static.title = '';
7241  * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
7242  * Normally only the icon is displayed, or only the label if no icon is given.
7244  * @static
7245  * @inheritable
7246  * @property {boolean}
7247  */
7248 OO.ui.Tool.static.displayBothIconAndLabel = false;
7251  * Add tool to catch-all groups automatically.
7253  * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
7254  * can be included in a toolgroup using the wildcard selector, an asterisk (*).
7256  * @static
7257  * @inheritable
7258  * @property {boolean}
7259  */
7260 OO.ui.Tool.static.autoAddToCatchall = true;
7263  * Add tool to named groups automatically.
7265  * By default, tools that are configured with a static ‘group’ property are added
7266  * to that group and will be selected when the symbolic name of the group is specified (e.g., when
7267  * toolgroups include tools by group name).
7269  * @static
7270  * @property {boolean}
7271  * @inheritable
7272  */
7273 OO.ui.Tool.static.autoAddToGroup = true;
7276  * Check if this tool is compatible with given data.
7278  * This is a stub that can be overriden to provide support for filtering tools based on an
7279  * arbitrary piece of information  (e.g., where the cursor is in a document). The implementation
7280  * must also call this method so that the compatibility check can be performed.
7282  * @static
7283  * @inheritable
7284  * @param {Mixed} data Data to check
7285  * @return {boolean} Tool can be used with data
7286  */
7287 OO.ui.Tool.static.isCompatibleWith = function () {
7288         return false;
7291 /* Methods */
7294  * Handle the toolbar state being updated.
7296  * This is an abstract method that must be overridden in a concrete subclass.
7298  * @protected
7299  * @abstract
7300  */
7301 OO.ui.Tool.prototype.onUpdateState = function () {
7302         throw new Error(
7303                 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
7304         );
7308  * Handle the tool being selected.
7310  * This is an abstract method that must be overridden in a concrete subclass.
7312  * @protected
7313  * @abstract
7314  */
7315 OO.ui.Tool.prototype.onSelect = function () {
7316         throw new Error(
7317                 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
7318         );
7322  * Check if the tool is active.
7324  * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
7325  * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
7327  * @return {boolean} Tool is active
7328  */
7329 OO.ui.Tool.prototype.isActive = function () {
7330         return this.active;
7334  * Make the tool appear active or inactive.
7336  * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
7337  * appear pressed or not.
7339  * @param {boolean} state Make tool appear active
7340  */
7341 OO.ui.Tool.prototype.setActive = function ( state ) {
7342         this.active = !!state;
7343         if ( this.active ) {
7344                 this.$element.addClass( 'oo-ui-tool-active' );
7345         } else {
7346                 this.$element.removeClass( 'oo-ui-tool-active' );
7347         }
7351  * Set the tool #title.
7353  * @param {string|Function} title Title text or a function that returns text
7354  * @chainable
7355  */
7356 OO.ui.Tool.prototype.setTitle = function ( title ) {
7357         this.title = OO.ui.resolveMsg( title );
7358         this.updateTitle();
7359         return this;
7363  * Get the tool #title.
7365  * @return {string} Title text
7366  */
7367 OO.ui.Tool.prototype.getTitle = function () {
7368         return this.title;
7372  * Get the tool's symbolic name.
7374  * @return {string} Symbolic name of tool
7375  */
7376 OO.ui.Tool.prototype.getName = function () {
7377         return this.constructor.static.name;
7381  * Update the title.
7382  */
7383 OO.ui.Tool.prototype.updateTitle = function () {
7384         var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
7385                 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
7386                 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
7387                 tooltipParts = [];
7389         this.$title.text( this.title );
7390         this.$accel.text( accel );
7392         if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
7393                 tooltipParts.push( this.title );
7394         }
7395         if ( accelTooltips && typeof accel === 'string' && accel.length ) {
7396                 tooltipParts.push( accel );
7397         }
7398         if ( tooltipParts.length ) {
7399                 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
7400         } else {
7401                 this.$link.removeAttr( 'title' );
7402         }
7406  * Destroy tool.
7408  * Destroying the tool removes all event handlers and the tool’s DOM elements.
7409  * Call this method whenever you are done using a tool.
7410  */
7411 OO.ui.Tool.prototype.destroy = function () {
7412         this.toolbar.disconnect( this );
7413         this.$element.remove();
7417  * Toolbars are complex interface components that permit users to easily access a variety
7418  * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
7419  * part of the toolbar, but not configured as tools.
7421  * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
7422  * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
7423  * picture’), and an icon.
7425  * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
7426  * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
7427  * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
7428  * any order, but each can only appear once in the toolbar.
7430  * The following is an example of a basic toolbar.
7432  *     @example
7433  *     // Example of a toolbar
7434  *     // Create the toolbar
7435  *     var toolFactory = new OO.ui.ToolFactory();
7436  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
7437  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7439  *     // We will be placing status text in this element when tools are used
7440  *     var $area = $( '<p>' ).text( 'Toolbar example' );
7442  *     // Define the tools that we're going to place in our toolbar
7444  *     // Create a class inheriting from OO.ui.Tool
7445  *     function PictureTool() {
7446  *         PictureTool.parent.apply( this, arguments );
7447  *     }
7448  *     OO.inheritClass( PictureTool, OO.ui.Tool );
7449  *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7450  *     // of 'icon' and 'title' (displayed icon and text).
7451  *     PictureTool.static.name = 'picture';
7452  *     PictureTool.static.icon = 'picture';
7453  *     PictureTool.static.title = 'Insert picture';
7454  *     // Defines the action that will happen when this tool is selected (clicked).
7455  *     PictureTool.prototype.onSelect = function () {
7456  *         $area.text( 'Picture tool clicked!' );
7457  *         // Never display this tool as "active" (selected).
7458  *         this.setActive( false );
7459  *     };
7460  *     // Make this tool available in our toolFactory and thus our toolbar
7461  *     toolFactory.register( PictureTool );
7463  *     // Register two more tools, nothing interesting here
7464  *     function SettingsTool() {
7465  *         SettingsTool.parent.apply( this, arguments );
7466  *     }
7467  *     OO.inheritClass( SettingsTool, OO.ui.Tool );
7468  *     SettingsTool.static.name = 'settings';
7469  *     SettingsTool.static.icon = 'settings';
7470  *     SettingsTool.static.title = 'Change settings';
7471  *     SettingsTool.prototype.onSelect = function () {
7472  *         $area.text( 'Settings tool clicked!' );
7473  *         this.setActive( false );
7474  *     };
7475  *     toolFactory.register( SettingsTool );
7477  *     // Register two more tools, nothing interesting here
7478  *     function StuffTool() {
7479  *         StuffTool.parent.apply( this, arguments );
7480  *     }
7481  *     OO.inheritClass( StuffTool, OO.ui.Tool );
7482  *     StuffTool.static.name = 'stuff';
7483  *     StuffTool.static.icon = 'ellipsis';
7484  *     StuffTool.static.title = 'More stuff';
7485  *     StuffTool.prototype.onSelect = function () {
7486  *         $area.text( 'More stuff tool clicked!' );
7487  *         this.setActive( false );
7488  *     };
7489  *     toolFactory.register( StuffTool );
7491  *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7492  *     // little popup window (a PopupWidget).
7493  *     function HelpTool( toolGroup, config ) {
7494  *         OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7495  *             padded: true,
7496  *             label: 'Help',
7497  *             head: true
7498  *         } }, config ) );
7499  *         this.popup.$body.append( '<p>I am helpful!</p>' );
7500  *     }
7501  *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
7502  *     HelpTool.static.name = 'help';
7503  *     HelpTool.static.icon = 'help';
7504  *     HelpTool.static.title = 'Help';
7505  *     toolFactory.register( HelpTool );
7507  *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7508  *     // used once (but not all defined tools must be used).
7509  *     toolbar.setup( [
7510  *         {
7511  *             // 'bar' tool groups display tools' icons only, side-by-side.
7512  *             type: 'bar',
7513  *             include: [ 'picture', 'help' ]
7514  *         },
7515  *         {
7516  *             // 'list' tool groups display both the titles and icons, in a dropdown list.
7517  *             type: 'list',
7518  *             indicator: 'down',
7519  *             label: 'More',
7520  *             include: [ 'settings', 'stuff' ]
7521  *         }
7522  *         // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
7523  *         // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
7524  *         // since it's more complicated to use. (See the next example snippet on this page.)
7525  *     ] );
7527  *     // Create some UI around the toolbar and place it in the document
7528  *     var frame = new OO.ui.PanelLayout( {
7529  *         expanded: false,
7530  *         framed: true
7531  *     } );
7532  *     var contentFrame = new OO.ui.PanelLayout( {
7533  *         expanded: false,
7534  *         padded: true
7535  *     } );
7536  *     frame.$element.append(
7537  *         toolbar.$element,
7538  *         contentFrame.$element.append( $area )
7539  *     );
7540  *     $( 'body' ).append( frame.$element );
7542  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
7543  *     // document.
7544  *     toolbar.initialize();
7546  * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
7547  * 'updateState' event.
7549  *     @example
7550  *     // Create the toolbar
7551  *     var toolFactory = new OO.ui.ToolFactory();
7552  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
7553  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7555  *     // We will be placing status text in this element when tools are used
7556  *     var $area = $( '<p>' ).text( 'Toolbar example' );
7558  *     // Define the tools that we're going to place in our toolbar
7560  *     // Create a class inheriting from OO.ui.Tool
7561  *     function PictureTool() {
7562  *         PictureTool.parent.apply( this, arguments );
7563  *     }
7564  *     OO.inheritClass( PictureTool, OO.ui.Tool );
7565  *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7566  *     // of 'icon' and 'title' (displayed icon and text).
7567  *     PictureTool.static.name = 'picture';
7568  *     PictureTool.static.icon = 'picture';
7569  *     PictureTool.static.title = 'Insert picture';
7570  *     // Defines the action that will happen when this tool is selected (clicked).
7571  *     PictureTool.prototype.onSelect = function () {
7572  *         $area.text( 'Picture tool clicked!' );
7573  *         // Never display this tool as "active" (selected).
7574  *         this.setActive( false );
7575  *     };
7576  *     // The toolbar can be synchronized with the state of some external stuff, like a text
7577  *     // editor's editing area, highlighting the tools (e.g. a 'bold' tool would be shown as active
7578  *     // when the text cursor was inside bolded text). Here we simply disable this feature.
7579  *     PictureTool.prototype.onUpdateState = function () {
7580  *     };
7581  *     // Make this tool available in our toolFactory and thus our toolbar
7582  *     toolFactory.register( PictureTool );
7584  *     // Register two more tools, nothing interesting here
7585  *     function SettingsTool() {
7586  *         SettingsTool.parent.apply( this, arguments );
7587  *         this.reallyActive = false;
7588  *     }
7589  *     OO.inheritClass( SettingsTool, OO.ui.Tool );
7590  *     SettingsTool.static.name = 'settings';
7591  *     SettingsTool.static.icon = 'settings';
7592  *     SettingsTool.static.title = 'Change settings';
7593  *     SettingsTool.prototype.onSelect = function () {
7594  *         $area.text( 'Settings tool clicked!' );
7595  *         // Toggle the active state on each click
7596  *         this.reallyActive = !this.reallyActive;
7597  *         this.setActive( this.reallyActive );
7598  *         // To update the menu label
7599  *         this.toolbar.emit( 'updateState' );
7600  *     };
7601  *     SettingsTool.prototype.onUpdateState = function () {
7602  *     };
7603  *     toolFactory.register( SettingsTool );
7605  *     // Register two more tools, nothing interesting here
7606  *     function StuffTool() {
7607  *         StuffTool.parent.apply( this, arguments );
7608  *         this.reallyActive = false;
7609  *     }
7610  *     OO.inheritClass( StuffTool, OO.ui.Tool );
7611  *     StuffTool.static.name = 'stuff';
7612  *     StuffTool.static.icon = 'ellipsis';
7613  *     StuffTool.static.title = 'More stuff';
7614  *     StuffTool.prototype.onSelect = function () {
7615  *         $area.text( 'More stuff tool clicked!' );
7616  *         // Toggle the active state on each click
7617  *         this.reallyActive = !this.reallyActive;
7618  *         this.setActive( this.reallyActive );
7619  *         // To update the menu label
7620  *         this.toolbar.emit( 'updateState' );
7621  *     };
7622  *     StuffTool.prototype.onUpdateState = function () {
7623  *     };
7624  *     toolFactory.register( StuffTool );
7626  *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7627  *     // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
7628  *     function HelpTool( toolGroup, config ) {
7629  *         OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7630  *             padded: true,
7631  *             label: 'Help',
7632  *             head: true
7633  *         } }, config ) );
7634  *         this.popup.$body.append( '<p>I am helpful!</p>' );
7635  *     }
7636  *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
7637  *     HelpTool.static.name = 'help';
7638  *     HelpTool.static.icon = 'help';
7639  *     HelpTool.static.title = 'Help';
7640  *     toolFactory.register( HelpTool );
7642  *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7643  *     // used once (but not all defined tools must be used).
7644  *     toolbar.setup( [
7645  *         {
7646  *             // 'bar' tool groups display tools' icons only, side-by-side.
7647  *             type: 'bar',
7648  *             include: [ 'picture', 'help' ]
7649  *         },
7650  *         {
7651  *             // 'menu' tool groups display both the titles and icons, in a dropdown menu.
7652  *             // Menu label indicates which items are selected.
7653  *             type: 'menu',
7654  *             indicator: 'down',
7655  *             include: [ 'settings', 'stuff' ]
7656  *         }
7657  *     ] );
7659  *     // Create some UI around the toolbar and place it in the document
7660  *     var frame = new OO.ui.PanelLayout( {
7661  *         expanded: false,
7662  *         framed: true
7663  *     } );
7664  *     var contentFrame = new OO.ui.PanelLayout( {
7665  *         expanded: false,
7666  *         padded: true
7667  *     } );
7668  *     frame.$element.append(
7669  *         toolbar.$element,
7670  *         contentFrame.$element.append( $area )
7671  *     );
7672  *     $( 'body' ).append( frame.$element );
7674  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
7675  *     // document.
7676  *     toolbar.initialize();
7677  *     toolbar.emit( 'updateState' );
7679  * @class
7680  * @extends OO.ui.Element
7681  * @mixins OO.EventEmitter
7682  * @mixins OO.ui.mixin.GroupElement
7684  * @constructor
7685  * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
7686  * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
7687  * @param {Object} [config] Configuration options
7688  * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
7689  *  in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
7690  *  the toolbar.
7691  * @cfg {boolean} [shadow] Add a shadow below the toolbar.
7692  */
7693 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
7694         // Allow passing positional parameters inside the config object
7695         if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
7696                 config = toolFactory;
7697                 toolFactory = config.toolFactory;
7698                 toolGroupFactory = config.toolGroupFactory;
7699         }
7701         // Configuration initialization
7702         config = config || {};
7704         // Parent constructor
7705         OO.ui.Toolbar.parent.call( this, config );
7707         // Mixin constructors
7708         OO.EventEmitter.call( this );
7709         OO.ui.mixin.GroupElement.call( this, config );
7711         // Properties
7712         this.toolFactory = toolFactory;
7713         this.toolGroupFactory = toolGroupFactory;
7714         this.groups = [];
7715         this.tools = {};
7716         this.$bar = $( '<div>' );
7717         this.$actions = $( '<div>' );
7718         this.initialized = false;
7719         this.onWindowResizeHandler = this.onWindowResize.bind( this );
7721         // Events
7722         this.$element
7723                 .add( this.$bar ).add( this.$group ).add( this.$actions )
7724                 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
7726         // Initialization
7727         this.$group.addClass( 'oo-ui-toolbar-tools' );
7728         if ( config.actions ) {
7729                 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
7730         }
7731         this.$bar
7732                 .addClass( 'oo-ui-toolbar-bar' )
7733                 .append( this.$group, '<div style="clear:both"></div>' );
7734         if ( config.shadow ) {
7735                 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
7736         }
7737         this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
7740 /* Setup */
7742 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
7743 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
7744 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
7746 /* Methods */
7749  * Get the tool factory.
7751  * @return {OO.ui.ToolFactory} Tool factory
7752  */
7753 OO.ui.Toolbar.prototype.getToolFactory = function () {
7754         return this.toolFactory;
7758  * Get the toolgroup factory.
7760  * @return {OO.Factory} Toolgroup factory
7761  */
7762 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
7763         return this.toolGroupFactory;
7767  * Handles mouse down events.
7769  * @private
7770  * @param {jQuery.Event} e Mouse down event
7771  */
7772 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
7773         var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
7774                 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
7775         if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
7776                 return false;
7777         }
7781  * Handle window resize event.
7783  * @private
7784  * @param {jQuery.Event} e Window resize event
7785  */
7786 OO.ui.Toolbar.prototype.onWindowResize = function () {
7787         this.$element.toggleClass(
7788                 'oo-ui-toolbar-narrow',
7789                 this.$bar.width() <= this.narrowThreshold
7790         );
7794  * Sets up handles and preloads required information for the toolbar to work.
7795  * This must be called after it is attached to a visible document and before doing anything else.
7796  */
7797 OO.ui.Toolbar.prototype.initialize = function () {
7798         if ( !this.initialized ) {
7799                 this.initialized = true;
7800                 this.narrowThreshold = this.$group.width() + this.$actions.width();
7801                 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
7802                 this.onWindowResize();
7803         }
7807  * Set up the toolbar.
7809  * The toolbar is set up with a list of toolgroup configurations that specify the type of
7810  * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
7811  * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
7812  * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
7814  * @param {Object.<string,Array>} groups List of toolgroup configurations
7815  * @param {Array|string} [groups.include] Tools to include in the toolgroup
7816  * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
7817  * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
7818  * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
7819  */
7820 OO.ui.Toolbar.prototype.setup = function ( groups ) {
7821         var i, len, type, group,
7822                 items = [],
7823                 defaultType = 'bar';
7825         // Cleanup previous groups
7826         this.reset();
7828         // Build out new groups
7829         for ( i = 0, len = groups.length; i < len; i++ ) {
7830                 group = groups[ i ];
7831                 if ( group.include === '*' ) {
7832                         // Apply defaults to catch-all groups
7833                         if ( group.type === undefined ) {
7834                                 group.type = 'list';
7835                         }
7836                         if ( group.label === undefined ) {
7837                                 group.label = OO.ui.msg( 'ooui-toolbar-more' );
7838                         }
7839                 }
7840                 // Check type has been registered
7841                 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
7842                 items.push(
7843                         this.getToolGroupFactory().create( type, this, group )
7844                 );
7845         }
7846         this.addItems( items );
7850  * Remove all tools and toolgroups from the toolbar.
7851  */
7852 OO.ui.Toolbar.prototype.reset = function () {
7853         var i, len;
7855         this.groups = [];
7856         this.tools = {};
7857         for ( i = 0, len = this.items.length; i < len; i++ ) {
7858                 this.items[ i ].destroy();
7859         }
7860         this.clearItems();
7864  * Destroy the toolbar.
7866  * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
7867  * this method whenever you are done using a toolbar.
7868  */
7869 OO.ui.Toolbar.prototype.destroy = function () {
7870         $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
7871         this.reset();
7872         this.$element.remove();
7876  * Check if the tool is available.
7878  * Available tools are ones that have not yet been added to the toolbar.
7880  * @param {string} name Symbolic name of tool
7881  * @return {boolean} Tool is available
7882  */
7883 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
7884         return !this.tools[ name ];
7888  * Prevent tool from being used again.
7890  * @param {OO.ui.Tool} tool Tool to reserve
7891  */
7892 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
7893         this.tools[ tool.getName() ] = tool;
7897  * Allow tool to be used again.
7899  * @param {OO.ui.Tool} tool Tool to release
7900  */
7901 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
7902         delete this.tools[ tool.getName() ];
7906  * Get accelerator label for tool.
7908  * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
7909  * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
7910  * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
7912  * @param {string} name Symbolic name of tool
7913  * @return {string|undefined} Tool accelerator label if available
7914  */
7915 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
7916         return undefined;
7920  * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
7921  * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
7922  * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
7923  * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
7925  * Toolgroups can contain individual tools, groups of tools, or all available tools:
7927  * To include an individual tool (or array of individual tools), specify tools by symbolic name:
7929  *      include: [ 'tool-name' ] or [ { name: 'tool-name' }]
7931  * To include a group of tools, specify the group name. (The tool's static ‘group’ config is used to assign the tool to a group.)
7933  *      include: [ { group: 'group-name' } ]
7935  *  To include all tools that are not yet assigned to a toolgroup, use the catch-all selector, an asterisk (*):
7937  *      include: '*'
7939  * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
7940  * please see the [OOjs UI documentation on MediaWiki][1].
7942  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
7944  * @abstract
7945  * @class
7946  * @extends OO.ui.Widget
7947  * @mixins OO.ui.mixin.GroupElement
7949  * @constructor
7950  * @param {OO.ui.Toolbar} toolbar
7951  * @param {Object} [config] Configuration options
7952  * @cfg {Array|string} [include=[]] List of tools to include in the toolgroup.
7953  * @cfg {Array|string} [exclude=[]] List of tools to exclude from the toolgroup.
7954  * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning of the toolgroup.
7955  * @cfg {Array|string} [demote=[]] List of tools to demote to the end of the toolgroup.
7956  *  This setting is particularly useful when tools have been added to the toolgroup
7957  *  en masse (e.g., via the catch-all selector).
7958  */
7959 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
7960         // Allow passing positional parameters inside the config object
7961         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
7962                 config = toolbar;
7963                 toolbar = config.toolbar;
7964         }
7966         // Configuration initialization
7967         config = config || {};
7969         // Parent constructor
7970         OO.ui.ToolGroup.parent.call( this, config );
7972         // Mixin constructors
7973         OO.ui.mixin.GroupElement.call( this, config );
7975         // Properties
7976         this.toolbar = toolbar;
7977         this.tools = {};
7978         this.pressed = null;
7979         this.autoDisabled = false;
7980         this.include = config.include || [];
7981         this.exclude = config.exclude || [];
7982         this.promote = config.promote || [];
7983         this.demote = config.demote || [];
7984         this.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
7986         // Events
7987         this.$element.on( {
7988                 mousedown: this.onMouseKeyDown.bind( this ),
7989                 mouseup: this.onMouseKeyUp.bind( this ),
7990                 keydown: this.onMouseKeyDown.bind( this ),
7991                 keyup: this.onMouseKeyUp.bind( this ),
7992                 focus: this.onMouseOverFocus.bind( this ),
7993                 blur: this.onMouseOutBlur.bind( this ),
7994                 mouseover: this.onMouseOverFocus.bind( this ),
7995                 mouseout: this.onMouseOutBlur.bind( this )
7996         } );
7997         this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
7998         this.aggregate( { disable: 'itemDisable' } );
7999         this.connect( this, { itemDisable: 'updateDisabled' } );
8001         // Initialization
8002         this.$group.addClass( 'oo-ui-toolGroup-tools' );
8003         this.$element
8004                 .addClass( 'oo-ui-toolGroup' )
8005                 .append( this.$group );
8006         this.populate();
8009 /* Setup */
8011 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
8012 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
8014 /* Events */
8017  * @event update
8018  */
8020 /* Static Properties */
8023  * Show labels in tooltips.
8025  * @static
8026  * @inheritable
8027  * @property {boolean}
8028  */
8029 OO.ui.ToolGroup.static.titleTooltips = false;
8032  * Show acceleration labels in tooltips.
8034  * Note: The OOjs UI library does not include an accelerator system, but does contain
8035  * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
8036  * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
8037  * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
8039  * @static
8040  * @inheritable
8041  * @property {boolean}
8042  */
8043 OO.ui.ToolGroup.static.accelTooltips = false;
8046  * Automatically disable the toolgroup when all tools are disabled
8048  * @static
8049  * @inheritable
8050  * @property {boolean}
8051  */
8052 OO.ui.ToolGroup.static.autoDisable = true;
8054 /* Methods */
8057  * @inheritdoc
8058  */
8059 OO.ui.ToolGroup.prototype.isDisabled = function () {
8060         return this.autoDisabled || OO.ui.ToolGroup.parent.prototype.isDisabled.apply( this, arguments );
8064  * @inheritdoc
8065  */
8066 OO.ui.ToolGroup.prototype.updateDisabled = function () {
8067         var i, item, allDisabled = true;
8069         if ( this.constructor.static.autoDisable ) {
8070                 for ( i = this.items.length - 1; i >= 0; i-- ) {
8071                         item = this.items[ i ];
8072                         if ( !item.isDisabled() ) {
8073                                 allDisabled = false;
8074                                 break;
8075                         }
8076                 }
8077                 this.autoDisabled = allDisabled;
8078         }
8079         OO.ui.ToolGroup.parent.prototype.updateDisabled.apply( this, arguments );
8083  * Handle mouse down and key down events.
8085  * @protected
8086  * @param {jQuery.Event} e Mouse down or key down event
8087  */
8088 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
8089         if (
8090                 !this.isDisabled() &&
8091                 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
8092         ) {
8093                 this.pressed = this.getTargetTool( e );
8094                 if ( this.pressed ) {
8095                         this.pressed.setActive( true );
8096                         OO.ui.addCaptureEventListener( this.getElementDocument(), 'mouseup', this.onCapturedMouseKeyUpHandler );
8097                         OO.ui.addCaptureEventListener( this.getElementDocument(), 'keyup', this.onCapturedMouseKeyUpHandler );
8098                 }
8099                 return false;
8100         }
8104  * Handle captured mouse up and key up events.
8106  * @protected
8107  * @param {Event} e Mouse up or key up event
8108  */
8109 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
8110         OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup', this.onCapturedMouseKeyUpHandler );
8111         OO.ui.removeCaptureEventListener( this.getElementDocument(), 'keyup', this.onCapturedMouseKeyUpHandler );
8112         // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
8113         // released, but since `this.pressed` will no longer be true, the second call will be ignored.
8114         this.onMouseKeyUp( e );
8118  * Handle mouse up and key up events.
8120  * @protected
8121  * @param {jQuery.Event} e Mouse up or key up event
8122  */
8123 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
8124         var tool = this.getTargetTool( e );
8126         if (
8127                 !this.isDisabled() && this.pressed && this.pressed === tool &&
8128                 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
8129         ) {
8130                 this.pressed.onSelect();
8131                 this.pressed = null;
8132                 return false;
8133         }
8135         this.pressed = null;
8139  * Handle mouse over and focus events.
8141  * @protected
8142  * @param {jQuery.Event} e Mouse over or focus event
8143  */
8144 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
8145         var tool = this.getTargetTool( e );
8147         if ( this.pressed && this.pressed === tool ) {
8148                 this.pressed.setActive( true );
8149         }
8153  * Handle mouse out and blur events.
8155  * @protected
8156  * @param {jQuery.Event} e Mouse out or blur event
8157  */
8158 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
8159         var tool = this.getTargetTool( e );
8161         if ( this.pressed && this.pressed === tool ) {
8162                 this.pressed.setActive( false );
8163         }
8167  * Get the closest tool to a jQuery.Event.
8169  * Only tool links are considered, which prevents other elements in the tool such as popups from
8170  * triggering tool group interactions.
8172  * @private
8173  * @param {jQuery.Event} e
8174  * @return {OO.ui.Tool|null} Tool, `null` if none was found
8175  */
8176 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
8177         var tool,
8178                 $item = $( e.target ).closest( '.oo-ui-tool-link' );
8180         if ( $item.length ) {
8181                 tool = $item.parent().data( 'oo-ui-tool' );
8182         }
8184         return tool && !tool.isDisabled() ? tool : null;
8188  * Handle tool registry register events.
8190  * If a tool is registered after the group is created, we must repopulate the list to account for:
8192  * - a tool being added that may be included
8193  * - a tool already included being overridden
8195  * @protected
8196  * @param {string} name Symbolic name of tool
8197  */
8198 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
8199         this.populate();
8203  * Get the toolbar that contains the toolgroup.
8205  * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
8206  */
8207 OO.ui.ToolGroup.prototype.getToolbar = function () {
8208         return this.toolbar;
8212  * Add and remove tools based on configuration.
8213  */
8214 OO.ui.ToolGroup.prototype.populate = function () {
8215         var i, len, name, tool,
8216                 toolFactory = this.toolbar.getToolFactory(),
8217                 names = {},
8218                 add = [],
8219                 remove = [],
8220                 list = this.toolbar.getToolFactory().getTools(
8221                         this.include, this.exclude, this.promote, this.demote
8222                 );
8224         // Build a list of needed tools
8225         for ( i = 0, len = list.length; i < len; i++ ) {
8226                 name = list[ i ];
8227                 if (
8228                         // Tool exists
8229                         toolFactory.lookup( name ) &&
8230                         // Tool is available or is already in this group
8231                         ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
8232                 ) {
8233                         // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
8234                         // creating it, but we can't call reserveTool() yet because we haven't created the tool.
8235                         this.toolbar.tools[ name ] = true;
8236                         tool = this.tools[ name ];
8237                         if ( !tool ) {
8238                                 // Auto-initialize tools on first use
8239                                 this.tools[ name ] = tool = toolFactory.create( name, this );
8240                                 tool.updateTitle();
8241                         }
8242                         this.toolbar.reserveTool( tool );
8243                         add.push( tool );
8244                         names[ name ] = true;
8245                 }
8246         }
8247         // Remove tools that are no longer needed
8248         for ( name in this.tools ) {
8249                 if ( !names[ name ] ) {
8250                         this.tools[ name ].destroy();
8251                         this.toolbar.releaseTool( this.tools[ name ] );
8252                         remove.push( this.tools[ name ] );
8253                         delete this.tools[ name ];
8254                 }
8255         }
8256         if ( remove.length ) {
8257                 this.removeItems( remove );
8258         }
8259         // Update emptiness state
8260         if ( add.length ) {
8261                 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
8262         } else {
8263                 this.$element.addClass( 'oo-ui-toolGroup-empty' );
8264         }
8265         // Re-add tools (moving existing ones to new locations)
8266         this.addItems( add );
8267         // Disabled state may depend on items
8268         this.updateDisabled();
8272  * Destroy toolgroup.
8273  */
8274 OO.ui.ToolGroup.prototype.destroy = function () {
8275         var name;
8277         this.clearItems();
8278         this.toolbar.getToolFactory().disconnect( this );
8279         for ( name in this.tools ) {
8280                 this.toolbar.releaseTool( this.tools[ name ] );
8281                 this.tools[ name ].disconnect( this ).destroy();
8282                 delete this.tools[ name ];
8283         }
8284         this.$element.remove();
8288  * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
8289  * consists of a header that contains the dialog title, a body with the message, and a footer that
8290  * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
8291  * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
8293  * There are two basic types of message dialogs, confirmation and alert:
8295  * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
8296  *  more details about the consequences.
8297  * - **alert**: the dialog title describes which event occurred and the message provides more information
8298  *  about why the event occurred.
8300  * The MessageDialog class specifies two actions: ‘accept’, the primary
8301  * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
8302  * passing along the selected action.
8304  * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
8306  *     @example
8307  *     // Example: Creating and opening a message dialog window.
8308  *     var messageDialog = new OO.ui.MessageDialog();
8310  *     // Create and append a window manager.
8311  *     var windowManager = new OO.ui.WindowManager();
8312  *     $( 'body' ).append( windowManager.$element );
8313  *     windowManager.addWindows( [ messageDialog ] );
8314  *     // Open the window.
8315  *     windowManager.openWindow( messageDialog, {
8316  *         title: 'Basic message dialog',
8317  *         message: 'This is the message'
8318  *     } );
8320  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
8322  * @class
8323  * @extends OO.ui.Dialog
8325  * @constructor
8326  * @param {Object} [config] Configuration options
8327  */
8328 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
8329         // Parent constructor
8330         OO.ui.MessageDialog.parent.call( this, config );
8332         // Properties
8333         this.verticalActionLayout = null;
8335         // Initialization
8336         this.$element.addClass( 'oo-ui-messageDialog' );
8339 /* Setup */
8341 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
8343 /* Static Properties */
8345 OO.ui.MessageDialog.static.name = 'message';
8347 OO.ui.MessageDialog.static.size = 'small';
8349 OO.ui.MessageDialog.static.verbose = false;
8352  * Dialog title.
8354  * The title of a confirmation dialog describes what a progressive action will do. The
8355  * title of an alert dialog describes which event occurred.
8357  * @static
8358  * @inheritable
8359  * @property {jQuery|string|Function|null}
8360  */
8361 OO.ui.MessageDialog.static.title = null;
8364  * The message displayed in the dialog body.
8366  * A confirmation message describes the consequences of a progressive action. An alert
8367  * message describes why an event occurred.
8369  * @static
8370  * @inheritable
8371  * @property {jQuery|string|Function|null}
8372  */
8373 OO.ui.MessageDialog.static.message = null;
8375 OO.ui.MessageDialog.static.actions = [
8376         { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
8377         { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
8380 /* Methods */
8383  * @inheritdoc
8384  */
8385 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
8386         OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
8388         // Events
8389         this.manager.connect( this, {
8390                 resize: 'onResize'
8391         } );
8393         return this;
8397  * @inheritdoc
8398  */
8399 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
8400         this.fitActions();
8401         return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
8405  * Handle window resized events.
8407  * @private
8408  */
8409 OO.ui.MessageDialog.prototype.onResize = function () {
8410         var dialog = this;
8411         dialog.fitActions();
8412         // Wait for CSS transition to finish and do it again :(
8413         setTimeout( function () {
8414                 dialog.fitActions();
8415         }, 300 );
8419  * Toggle action layout between vertical and horizontal.
8421  * @private
8422  * @param {boolean} [value] Layout actions vertically, omit to toggle
8423  * @chainable
8424  */
8425 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
8426         value = value === undefined ? !this.verticalActionLayout : !!value;
8428         if ( value !== this.verticalActionLayout ) {
8429                 this.verticalActionLayout = value;
8430                 this.$actions
8431                         .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
8432                         .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
8433         }
8435         return this;
8439  * @inheritdoc
8440  */
8441 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
8442         if ( action ) {
8443                 return new OO.ui.Process( function () {
8444                         this.close( { action: action } );
8445                 }, this );
8446         }
8447         return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
8451  * @inheritdoc
8453  * @param {Object} [data] Dialog opening data
8454  * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
8455  * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
8456  * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
8457  * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
8458  *   action item
8459  */
8460 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
8461         data = data || {};
8463         // Parent method
8464         return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
8465                 .next( function () {
8466                         this.title.setLabel(
8467                                 data.title !== undefined ? data.title : this.constructor.static.title
8468                         );
8469                         this.message.setLabel(
8470                                 data.message !== undefined ? data.message : this.constructor.static.message
8471                         );
8472                         this.message.$element.toggleClass(
8473                                 'oo-ui-messageDialog-message-verbose',
8474                                 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
8475                         );
8476                 }, this );
8480  * @inheritdoc
8481  */
8482 OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
8483         data = data || {};
8485         // Parent method
8486         return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data )
8487                 .next( function () {
8488                         // Focus the primary action button
8489                         var actions = this.actions.get();
8490                         actions = actions.filter( function ( action ) {
8491                                 return action.getFlags().indexOf( 'primary' ) > -1;
8492                         } );
8493                         if ( actions.length > 0 ) {
8494                                 actions[ 0 ].$button.focus();
8495                         }
8496                 }, this );
8500  * @inheritdoc
8501  */
8502 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
8503         var bodyHeight, oldOverflow,
8504                 $scrollable = this.container.$element;
8506         oldOverflow = $scrollable[ 0 ].style.overflow;
8507         $scrollable[ 0 ].style.overflow = 'hidden';
8509         OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
8511         bodyHeight = this.text.$element.outerHeight( true );
8512         $scrollable[ 0 ].style.overflow = oldOverflow;
8514         return bodyHeight;
8518  * @inheritdoc
8519  */
8520 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
8521         var $scrollable = this.container.$element;
8522         OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
8524         // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
8525         // Need to do it after transition completes (250ms), add 50ms just in case.
8526         setTimeout( function () {
8527                 var oldOverflow = $scrollable[ 0 ].style.overflow;
8528                 $scrollable[ 0 ].style.overflow = 'hidden';
8530                 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
8532                 $scrollable[ 0 ].style.overflow = oldOverflow;
8533         }, 300 );
8535         return this;
8539  * @inheritdoc
8540  */
8541 OO.ui.MessageDialog.prototype.initialize = function () {
8542         // Parent method
8543         OO.ui.MessageDialog.parent.prototype.initialize.call( this );
8545         // Properties
8546         this.$actions = $( '<div>' );
8547         this.container = new OO.ui.PanelLayout( {
8548                 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
8549         } );
8550         this.text = new OO.ui.PanelLayout( {
8551                 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
8552         } );
8553         this.message = new OO.ui.LabelWidget( {
8554                 classes: [ 'oo-ui-messageDialog-message' ]
8555         } );
8557         // Initialization
8558         this.title.$element.addClass( 'oo-ui-messageDialog-title' );
8559         this.$content.addClass( 'oo-ui-messageDialog-content' );
8560         this.container.$element.append( this.text.$element );
8561         this.text.$element.append( this.title.$element, this.message.$element );
8562         this.$body.append( this.container.$element );
8563         this.$actions.addClass( 'oo-ui-messageDialog-actions' );
8564         this.$foot.append( this.$actions );
8568  * @inheritdoc
8569  */
8570 OO.ui.MessageDialog.prototype.attachActions = function () {
8571         var i, len, other, special, others;
8573         // Parent method
8574         OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
8576         special = this.actions.getSpecial();
8577         others = this.actions.getOthers();
8579         if ( special.safe ) {
8580                 this.$actions.append( special.safe.$element );
8581                 special.safe.toggleFramed( false );
8582         }
8583         if ( others.length ) {
8584                 for ( i = 0, len = others.length; i < len; i++ ) {
8585                         other = others[ i ];
8586                         this.$actions.append( other.$element );
8587                         other.toggleFramed( false );
8588                 }
8589         }
8590         if ( special.primary ) {
8591                 this.$actions.append( special.primary.$element );
8592                 special.primary.toggleFramed( false );
8593         }
8595         if ( !this.isOpening() ) {
8596                 // If the dialog is currently opening, this will be called automatically soon.
8597                 // This also calls #fitActions.
8598                 this.updateSize();
8599         }
8603  * Fit action actions into columns or rows.
8605  * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
8607  * @private
8608  */
8609 OO.ui.MessageDialog.prototype.fitActions = function () {
8610         var i, len, action,
8611                 previous = this.verticalActionLayout,
8612                 actions = this.actions.get();
8614         // Detect clipping
8615         this.toggleVerticalActionLayout( false );
8616         for ( i = 0, len = actions.length; i < len; i++ ) {
8617                 action = actions[ i ];
8618                 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
8619                         this.toggleVerticalActionLayout( true );
8620                         break;
8621                 }
8622         }
8624         // Move the body out of the way of the foot
8625         this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8627         if ( this.verticalActionLayout !== previous ) {
8628                 // We changed the layout, window height might need to be updated.
8629                 this.updateSize();
8630         }
8634  * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
8635  * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
8636  * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
8637  * relevant. The ProcessDialog class is always extended and customized with the actions and content
8638  * required for each process.
8640  * The process dialog box consists of a header that visually represents the ‘working’ state of long
8641  * processes with an animation. The header contains the dialog title as well as
8642  * two {@link OO.ui.ActionWidget action widgets}:  a ‘safe’ action on the left (e.g., ‘Cancel’) and
8643  * a ‘primary’ action on the right (e.g., ‘Done’).
8645  * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
8646  * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
8648  *     @example
8649  *     // Example: Creating and opening a process dialog window.
8650  *     function MyProcessDialog( config ) {
8651  *         MyProcessDialog.parent.call( this, config );
8652  *     }
8653  *     OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
8655  *     MyProcessDialog.static.title = 'Process dialog';
8656  *     MyProcessDialog.static.actions = [
8657  *         { action: 'save', label: 'Done', flags: 'primary' },
8658  *         { label: 'Cancel', flags: 'safe' }
8659  *     ];
8661  *     MyProcessDialog.prototype.initialize = function () {
8662  *         MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
8663  *         this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
8664  *         this.content.$element.append( '<p>This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action)  on the right.</p>' );
8665  *         this.$body.append( this.content.$element );
8666  *     };
8667  *     MyProcessDialog.prototype.getActionProcess = function ( action ) {
8668  *         var dialog = this;
8669  *         if ( action ) {
8670  *             return new OO.ui.Process( function () {
8671  *                 dialog.close( { action: action } );
8672  *             } );
8673  *         }
8674  *         return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
8675  *     };
8677  *     var windowManager = new OO.ui.WindowManager();
8678  *     $( 'body' ).append( windowManager.$element );
8680  *     var dialog = new MyProcessDialog();
8681  *     windowManager.addWindows( [ dialog ] );
8682  *     windowManager.openWindow( dialog );
8684  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
8686  * @abstract
8687  * @class
8688  * @extends OO.ui.Dialog
8690  * @constructor
8691  * @param {Object} [config] Configuration options
8692  */
8693 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
8694         // Parent constructor
8695         OO.ui.ProcessDialog.parent.call( this, config );
8697         // Properties
8698         this.fitOnOpen = false;
8700         // Initialization
8701         this.$element.addClass( 'oo-ui-processDialog' );
8704 /* Setup */
8706 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
8708 /* Methods */
8711  * Handle dismiss button click events.
8713  * Hides errors.
8715  * @private
8716  */
8717 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
8718         this.hideErrors();
8722  * Handle retry button click events.
8724  * Hides errors and then tries again.
8726  * @private
8727  */
8728 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
8729         this.hideErrors();
8730         this.executeAction( this.currentAction );
8734  * @inheritdoc
8735  */
8736 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
8737         if ( this.actions.isSpecial( action ) ) {
8738                 this.fitLabel();
8739         }
8740         return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
8744  * @inheritdoc
8745  */
8746 OO.ui.ProcessDialog.prototype.initialize = function () {
8747         // Parent method
8748         OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
8750         // Properties
8751         this.$navigation = $( '<div>' );
8752         this.$location = $( '<div>' );
8753         this.$safeActions = $( '<div>' );
8754         this.$primaryActions = $( '<div>' );
8755         this.$otherActions = $( '<div>' );
8756         this.dismissButton = new OO.ui.ButtonWidget( {
8757                 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
8758         } );
8759         this.retryButton = new OO.ui.ButtonWidget();
8760         this.$errors = $( '<div>' );
8761         this.$errorsTitle = $( '<div>' );
8763         // Events
8764         this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
8765         this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
8767         // Initialization
8768         this.title.$element.addClass( 'oo-ui-processDialog-title' );
8769         this.$location
8770                 .append( this.title.$element )
8771                 .addClass( 'oo-ui-processDialog-location' );
8772         this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
8773         this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
8774         this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
8775         this.$errorsTitle
8776                 .addClass( 'oo-ui-processDialog-errors-title' )
8777                 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
8778         this.$errors
8779                 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
8780                 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
8781         this.$content
8782                 .addClass( 'oo-ui-processDialog-content' )
8783                 .append( this.$errors );
8784         this.$navigation
8785                 .addClass( 'oo-ui-processDialog-navigation' )
8786                 .append( this.$safeActions, this.$location, this.$primaryActions );
8787         this.$head.append( this.$navigation );
8788         this.$foot.append( this.$otherActions );
8792  * @inheritdoc
8793  */
8794 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
8795         var i, len, widgets = [];
8796         for ( i = 0, len = actions.length; i < len; i++ ) {
8797                 widgets.push(
8798                         new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
8799                 );
8800         }
8801         return widgets;
8805  * @inheritdoc
8806  */
8807 OO.ui.ProcessDialog.prototype.attachActions = function () {
8808         var i, len, other, special, others;
8810         // Parent method
8811         OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
8813         special = this.actions.getSpecial();
8814         others = this.actions.getOthers();
8815         if ( special.primary ) {
8816                 this.$primaryActions.append( special.primary.$element );
8817         }
8818         for ( i = 0, len = others.length; i < len; i++ ) {
8819                 other = others[ i ];
8820                 this.$otherActions.append( other.$element );
8821         }
8822         if ( special.safe ) {
8823                 this.$safeActions.append( special.safe.$element );
8824         }
8826         this.fitLabel();
8827         this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8831  * @inheritdoc
8832  */
8833 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
8834         var process = this;
8835         return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
8836                 .fail( function ( errors ) {
8837                         process.showErrors( errors || [] );
8838                 } );
8842  * @inheritdoc
8843  */
8844 OO.ui.ProcessDialog.prototype.setDimensions = function () {
8845         // Parent method
8846         OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments );
8848         this.fitLabel();
8852  * Fit label between actions.
8854  * @private
8855  * @chainable
8856  */
8857 OO.ui.ProcessDialog.prototype.fitLabel = function () {
8858         var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth,
8859                 size = this.getSizeProperties();
8861         if ( typeof size.width !== 'number' ) {
8862                 if ( this.isOpened() ) {
8863                         navigationWidth = this.$head.width() - 20;
8864                 } else if ( this.isOpening() ) {
8865                         if ( !this.fitOnOpen ) {
8866                                 // Size is relative and the dialog isn't open yet, so wait.
8867                                 this.manager.opening.done( this.fitLabel.bind( this ) );
8868                                 this.fitOnOpen = true;
8869                         }
8870                         return;
8871                 } else {
8872                         return;
8873                 }
8874         } else {
8875                 navigationWidth = size.width - 20;
8876         }
8878         safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
8879         primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
8880         biggerWidth = Math.max( safeWidth, primaryWidth );
8882         labelWidth = this.title.$element.width();
8884         if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
8885                 // We have enough space to center the label
8886                 leftWidth = rightWidth = biggerWidth;
8887         } else {
8888                 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
8889                 if ( this.getDir() === 'ltr' ) {
8890                         leftWidth = safeWidth;
8891                         rightWidth = primaryWidth;
8892                 } else {
8893                         leftWidth = primaryWidth;
8894                         rightWidth = safeWidth;
8895                 }
8896         }
8898         this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
8900         return this;
8904  * Handle errors that occurred during accept or reject processes.
8906  * @private
8907  * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
8908  */
8909 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
8910         var i, len, $item, actions,
8911                 items = [],
8912                 abilities = {},
8913                 recoverable = true,
8914                 warning = false;
8916         if ( errors instanceof OO.ui.Error ) {
8917                 errors = [ errors ];
8918         }
8920         for ( i = 0, len = errors.length; i < len; i++ ) {
8921                 if ( !errors[ i ].isRecoverable() ) {
8922                         recoverable = false;
8923                 }
8924                 if ( errors[ i ].isWarning() ) {
8925                         warning = true;
8926                 }
8927                 $item = $( '<div>' )
8928                         .addClass( 'oo-ui-processDialog-error' )
8929                         .append( errors[ i ].getMessage() );
8930                 items.push( $item[ 0 ] );
8931         }
8932         this.$errorItems = $( items );
8933         if ( recoverable ) {
8934                 abilities[ this.currentAction ] = true;
8935                 // Copy the flags from the first matching action
8936                 actions = this.actions.get( { actions: this.currentAction } );
8937                 if ( actions.length ) {
8938                         this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
8939                 }
8940         } else {
8941                 abilities[ this.currentAction ] = false;
8942                 this.actions.setAbilities( abilities );
8943         }
8944         if ( warning ) {
8945                 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
8946         } else {
8947                 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
8948         }
8949         this.retryButton.toggle( recoverable );
8950         this.$errorsTitle.after( this.$errorItems );
8951         this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
8955  * Hide errors.
8957  * @private
8958  */
8959 OO.ui.ProcessDialog.prototype.hideErrors = function () {
8960         this.$errors.addClass( 'oo-ui-element-hidden' );
8961         if ( this.$errorItems ) {
8962                 this.$errorItems.remove();
8963                 this.$errorItems = null;
8964         }
8968  * @inheritdoc
8969  */
8970 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
8971         // Parent method
8972         return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
8973                 .first( function () {
8974                         // Make sure to hide errors
8975                         this.hideErrors();
8976                         this.fitOnOpen = false;
8977                 }, this );
8981  * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
8982  * which is a widget that is specified by reference before any optional configuration settings.
8984  * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
8986  * - **left**: The label is placed before the field-widget and aligned with the left margin.
8987  *   A left-alignment is used for forms with many fields.
8988  * - **right**: The label is placed before the field-widget and aligned to the right margin.
8989  *   A right-alignment is used for long but familiar forms which users tab through,
8990  *   verifying the current field with a quick glance at the label.
8991  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8992  *   that users fill out from top to bottom.
8993  * - **inline**: The label is placed after the field-widget and aligned to the left.
8994  *   An inline-alignment is best used with checkboxes or radio buttons.
8996  * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8997  * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8999  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9000  * @class
9001  * @extends OO.ui.Layout
9002  * @mixins OO.ui.mixin.LabelElement
9003  * @mixins OO.ui.mixin.TitledElement
9005  * @constructor
9006  * @param {OO.ui.Widget} fieldWidget Field widget
9007  * @param {Object} [config] Configuration options
9008  * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
9009  * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
9010  *  The array may contain strings or OO.ui.HtmlSnippet instances.
9011  * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
9012  *  The array may contain strings or OO.ui.HtmlSnippet instances.
9013  * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
9014  *  in the upper-right corner of the rendered field; clicking it will display the text in a popup.
9015  *  For important messages, you are advised to use `notices`, as they are always shown.
9017  * @throws {Error} An error is thrown if no widget is specified
9018  */
9019 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
9020         var hasInputWidget, div, i;
9022         // Allow passing positional parameters inside the config object
9023         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
9024                 config = fieldWidget;
9025                 fieldWidget = config.fieldWidget;
9026         }
9028         // Make sure we have required constructor arguments
9029         if ( fieldWidget === undefined ) {
9030                 throw new Error( 'Widget not found' );
9031         }
9033         hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
9035         // Configuration initialization
9036         config = $.extend( { align: 'left' }, config );
9038         // Parent constructor
9039         OO.ui.FieldLayout.parent.call( this, config );
9041         // Mixin constructors
9042         OO.ui.mixin.LabelElement.call( this, config );
9043         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
9045         // Properties
9046         this.fieldWidget = fieldWidget;
9047         this.errors = config.errors || [];
9048         this.notices = config.notices || [];
9049         this.$field = $( '<div>' );
9050         this.$messages = $( '<ul>' );
9051         this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
9052         this.align = null;
9053         if ( config.help ) {
9054                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
9055                         classes: [ 'oo-ui-fieldLayout-help' ],
9056                         framed: false,
9057                         icon: 'info'
9058                 } );
9060                 div = $( '<div>' );
9061                 if ( config.help instanceof OO.ui.HtmlSnippet ) {
9062                         div.html( config.help.toString() );
9063                 } else {
9064                         div.text( config.help );
9065                 }
9066                 this.popupButtonWidget.getPopup().$body.append(
9067                         div.addClass( 'oo-ui-fieldLayout-help-content' )
9068                 );
9069                 this.$help = this.popupButtonWidget.$element;
9070         } else {
9071                 this.$help = $( [] );
9072         }
9074         // Events
9075         if ( hasInputWidget ) {
9076                 this.$label.on( 'click', this.onLabelClick.bind( this ) );
9077         }
9078         this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
9080         // Initialization
9081         this.$element
9082                 .addClass( 'oo-ui-fieldLayout' )
9083                 .append( this.$help, this.$body );
9084         if ( this.errors.length || this.notices.length ) {
9085                 this.$element.append( this.$messages );
9086         }
9087         this.$body.addClass( 'oo-ui-fieldLayout-body' );
9088         this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
9089         this.$field
9090                 .addClass( 'oo-ui-fieldLayout-field' )
9091                 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
9092                 .append( this.fieldWidget.$element );
9094         for ( i = 0; i < this.notices.length; i++ ) {
9095                 this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
9096         }
9097         for ( i = 0; i < this.errors.length; i++ ) {
9098                 this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
9099         }
9101         this.setAlignment( config.align );
9104 /* Setup */
9106 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
9107 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
9108 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.TitledElement );
9110 /* Methods */
9113  * Handle field disable events.
9115  * @private
9116  * @param {boolean} value Field is disabled
9117  */
9118 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
9119         this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
9123  * Handle label mouse click events.
9125  * @private
9126  * @param {jQuery.Event} e Mouse click event
9127  */
9128 OO.ui.FieldLayout.prototype.onLabelClick = function () {
9129         this.fieldWidget.simulateLabelClick();
9130         return false;
9134  * Get the widget contained by the field.
9136  * @return {OO.ui.Widget} Field widget
9137  */
9138 OO.ui.FieldLayout.prototype.getField = function () {
9139         return this.fieldWidget;
9143  * @param {string} kind 'error' or 'notice'
9144  * @param {string|OO.ui.HtmlSnippet} text
9145  * @return {jQuery}
9146  */
9147 OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
9148         var $listItem, $icon, message;
9149         $listItem = $( '<li>' );
9150         if ( kind === 'error' ) {
9151                 $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
9152         } else if ( kind === 'notice' ) {
9153                 $icon = new OO.ui.IconWidget( { icon: 'info' } ).$element;
9154         } else {
9155                 $icon = '';
9156         }
9157         message = new OO.ui.LabelWidget( { label: text } );
9158         $listItem
9159                 .append( $icon, message.$element )
9160                 .addClass( 'oo-ui-fieldLayout-messages-' + kind );
9161         return $listItem;
9165  * Set the field alignment mode.
9167  * @private
9168  * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
9169  * @chainable
9170  */
9171 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
9172         if ( value !== this.align ) {
9173                 // Default to 'left'
9174                 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
9175                         value = 'left';
9176                 }
9177                 // Reorder elements
9178                 if ( value === 'inline' ) {
9179                         this.$body.append( this.$field, this.$label );
9180                 } else {
9181                         this.$body.append( this.$label, this.$field );
9182                 }
9183                 // Set classes. The following classes can be used here:
9184                 // * oo-ui-fieldLayout-align-left
9185                 // * oo-ui-fieldLayout-align-right
9186                 // * oo-ui-fieldLayout-align-top
9187                 // * oo-ui-fieldLayout-align-inline
9188                 if ( this.align ) {
9189                         this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
9190                 }
9191                 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
9192                 this.align = value;
9193         }
9195         return this;
9199  * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
9200  * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
9201  * is required and is specified before any optional configuration settings.
9203  * Labels can be aligned in one of four ways:
9205  * - **left**: The label is placed before the field-widget and aligned with the left margin.
9206  *   A left-alignment is used for forms with many fields.
9207  * - **right**: The label is placed before the field-widget and aligned to the right margin.
9208  *   A right-alignment is used for long but familiar forms which users tab through,
9209  *   verifying the current field with a quick glance at the label.
9210  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9211  *   that users fill out from top to bottom.
9212  * - **inline**: The label is placed after the field-widget and aligned to the left.
9213  *   An inline-alignment is best used with checkboxes or radio buttons.
9215  * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
9216  * text is specified.
9218  *     @example
9219  *     // Example of an ActionFieldLayout
9220  *     var actionFieldLayout = new OO.ui.ActionFieldLayout(
9221  *         new OO.ui.TextInputWidget( {
9222  *             placeholder: 'Field widget'
9223  *         } ),
9224  *         new OO.ui.ButtonWidget( {
9225  *             label: 'Button'
9226  *         } ),
9227  *         {
9228  *             label: 'An ActionFieldLayout. This label is aligned top',
9229  *             align: 'top',
9230  *             help: 'This is help text'
9231  *         }
9232  *     );
9234  *     $( 'body' ).append( actionFieldLayout.$element );
9236  * @class
9237  * @extends OO.ui.FieldLayout
9239  * @constructor
9240  * @param {OO.ui.Widget} fieldWidget Field widget
9241  * @param {OO.ui.ButtonWidget} buttonWidget Button widget
9242  */
9243 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
9244         // Allow passing positional parameters inside the config object
9245         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
9246                 config = fieldWidget;
9247                 fieldWidget = config.fieldWidget;
9248                 buttonWidget = config.buttonWidget;
9249         }
9251         // Parent constructor
9252         OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
9254         // Properties
9255         this.buttonWidget = buttonWidget;
9256         this.$button = $( '<div>' );
9257         this.$input = $( '<div>' );
9259         // Initialization
9260         this.$element
9261                 .addClass( 'oo-ui-actionFieldLayout' );
9262         this.$button
9263                 .addClass( 'oo-ui-actionFieldLayout-button' )
9264                 .append( this.buttonWidget.$element );
9265         this.$input
9266                 .addClass( 'oo-ui-actionFieldLayout-input' )
9267                 .append( this.fieldWidget.$element );
9268         this.$field
9269                 .append( this.$input, this.$button );
9272 /* Setup */
9274 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
9277  * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
9278  * which each contain an individual widget and, optionally, a label. Each Fieldset can be
9279  * configured with a label as well. For more information and examples,
9280  * please see the [OOjs UI documentation on MediaWiki][1].
9282  *     @example
9283  *     // Example of a fieldset layout
9284  *     var input1 = new OO.ui.TextInputWidget( {
9285  *         placeholder: 'A text input field'
9286  *     } );
9288  *     var input2 = new OO.ui.TextInputWidget( {
9289  *         placeholder: 'A text input field'
9290  *     } );
9292  *     var fieldset = new OO.ui.FieldsetLayout( {
9293  *         label: 'Example of a fieldset layout'
9294  *     } );
9296  *     fieldset.addItems( [
9297  *         new OO.ui.FieldLayout( input1, {
9298  *             label: 'Field One'
9299  *         } ),
9300  *         new OO.ui.FieldLayout( input2, {
9301  *             label: 'Field Two'
9302  *         } )
9303  *     ] );
9304  *     $( 'body' ).append( fieldset.$element );
9306  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9308  * @class
9309  * @extends OO.ui.Layout
9310  * @mixins OO.ui.mixin.IconElement
9311  * @mixins OO.ui.mixin.LabelElement
9312  * @mixins OO.ui.mixin.GroupElement
9314  * @constructor
9315  * @param {Object} [config] Configuration options
9316  * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
9317  */
9318 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
9319         // Configuration initialization
9320         config = config || {};
9322         // Parent constructor
9323         OO.ui.FieldsetLayout.parent.call( this, config );
9325         // Mixin constructors
9326         OO.ui.mixin.IconElement.call( this, config );
9327         OO.ui.mixin.LabelElement.call( this, config );
9328         OO.ui.mixin.GroupElement.call( this, config );
9330         if ( config.help ) {
9331                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
9332                         classes: [ 'oo-ui-fieldsetLayout-help' ],
9333                         framed: false,
9334                         icon: 'info'
9335                 } );
9337                 this.popupButtonWidget.getPopup().$body.append(
9338                         $( '<div>' )
9339                                 .text( config.help )
9340                                 .addClass( 'oo-ui-fieldsetLayout-help-content' )
9341                 );
9342                 this.$help = this.popupButtonWidget.$element;
9343         } else {
9344                 this.$help = $( [] );
9345         }
9347         // Initialization
9348         this.$element
9349                 .addClass( 'oo-ui-fieldsetLayout' )
9350                 .prepend( this.$help, this.$icon, this.$label, this.$group );
9351         if ( Array.isArray( config.items ) ) {
9352                 this.addItems( config.items );
9353         }
9356 /* Setup */
9358 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
9359 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
9360 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
9361 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
9364  * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
9365  * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
9366  * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
9367  * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9369  * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
9370  * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
9371  * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
9372  * some fancier controls. Some controls have both regular and InputWidget variants, for example
9373  * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
9374  * often have simplified APIs to match the capabilities of HTML forms.
9375  * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
9377  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
9378  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9380  *     @example
9381  *     // Example of a form layout that wraps a fieldset layout
9382  *     var input1 = new OO.ui.TextInputWidget( {
9383  *         placeholder: 'Username'
9384  *     } );
9385  *     var input2 = new OO.ui.TextInputWidget( {
9386  *         placeholder: 'Password',
9387  *         type: 'password'
9388  *     } );
9389  *     var submit = new OO.ui.ButtonInputWidget( {
9390  *         label: 'Submit'
9391  *     } );
9393  *     var fieldset = new OO.ui.FieldsetLayout( {
9394  *         label: 'A form layout'
9395  *     } );
9396  *     fieldset.addItems( [
9397  *         new OO.ui.FieldLayout( input1, {
9398  *             label: 'Username',
9399  *             align: 'top'
9400  *         } ),
9401  *         new OO.ui.FieldLayout( input2, {
9402  *             label: 'Password',
9403  *             align: 'top'
9404  *         } ),
9405  *         new OO.ui.FieldLayout( submit )
9406  *     ] );
9407  *     var form = new OO.ui.FormLayout( {
9408  *         items: [ fieldset ],
9409  *         action: '/api/formhandler',
9410  *         method: 'get'
9411  *     } )
9412  *     $( 'body' ).append( form.$element );
9414  * @class
9415  * @extends OO.ui.Layout
9416  * @mixins OO.ui.mixin.GroupElement
9418  * @constructor
9419  * @param {Object} [config] Configuration options
9420  * @cfg {string} [method] HTML form `method` attribute
9421  * @cfg {string} [action] HTML form `action` attribute
9422  * @cfg {string} [enctype] HTML form `enctype` attribute
9423  * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
9424  */
9425 OO.ui.FormLayout = function OoUiFormLayout( config ) {
9426         // Configuration initialization
9427         config = config || {};
9429         // Parent constructor
9430         OO.ui.FormLayout.parent.call( this, config );
9432         // Mixin constructors
9433         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9435         // Events
9436         this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
9438         // Make sure the action is safe
9439         if ( config.action !== undefined && !OO.ui.isSafeUrl( config.action ) ) {
9440                 throw new Error( 'Potentially unsafe action provided: ' + config.action );
9441         }
9443         // Initialization
9444         this.$element
9445                 .addClass( 'oo-ui-formLayout' )
9446                 .attr( {
9447                         method: config.method,
9448                         action: config.action,
9449                         enctype: config.enctype
9450                 } );
9451         if ( Array.isArray( config.items ) ) {
9452                 this.addItems( config.items );
9453         }
9456 /* Setup */
9458 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
9459 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
9461 /* Events */
9464  * A 'submit' event is emitted when the form is submitted.
9466  * @event submit
9467  */
9469 /* Static Properties */
9471 OO.ui.FormLayout.static.tagName = 'form';
9473 /* Methods */
9476  * Handle form submit events.
9478  * @private
9479  * @param {jQuery.Event} e Submit event
9480  * @fires submit
9481  */
9482 OO.ui.FormLayout.prototype.onFormSubmit = function () {
9483         if ( this.emit( 'submit' ) ) {
9484                 return false;
9485         }
9489  * MenuLayouts combine a menu and a content {@link OO.ui.PanelLayout panel}. The menu is positioned relative to the content (after, before, top, or bottom)
9490  * and its size is customized with the #menuSize config. The content area will fill all remaining space.
9492  *     @example
9493  *     var menuLayout = new OO.ui.MenuLayout( {
9494  *         position: 'top'
9495  *     } ),
9496  *         menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
9497  *         contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
9498  *         select = new OO.ui.SelectWidget( {
9499  *             items: [
9500  *                 new OO.ui.OptionWidget( {
9501  *                     data: 'before',
9502  *                     label: 'Before',
9503  *                 } ),
9504  *                 new OO.ui.OptionWidget( {
9505  *                     data: 'after',
9506  *                     label: 'After',
9507  *                 } ),
9508  *                 new OO.ui.OptionWidget( {
9509  *                     data: 'top',
9510  *                     label: 'Top',
9511  *                 } ),
9512  *                 new OO.ui.OptionWidget( {
9513  *                     data: 'bottom',
9514  *                     label: 'Bottom',
9515  *                 } )
9516  *              ]
9517  *         } ).on( 'select', function ( item ) {
9518  *            menuLayout.setMenuPosition( item.getData() );
9519  *         } );
9521  *     menuLayout.$menu.append(
9522  *         menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
9523  *     );
9524  *     menuLayout.$content.append(
9525  *         contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
9526  *     );
9527  *     $( 'body' ).append( menuLayout.$element );
9529  * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
9530  * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
9531  * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
9532  * may be omitted.
9534  *     .oo-ui-menuLayout-menu {
9535  *         height: 200px;
9536  *         width: 200px;
9537  *     }
9538  *     .oo-ui-menuLayout-content {
9539  *         top: 200px;
9540  *         left: 200px;
9541  *         right: 200px;
9542  *         bottom: 200px;
9543  *     }
9545  * @class
9546  * @extends OO.ui.Layout
9548  * @constructor
9549  * @param {Object} [config] Configuration options
9550  * @cfg {boolean} [showMenu=true] Show menu
9551  * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
9552  */
9553 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
9554         // Configuration initialization
9555         config = $.extend( {
9556                 showMenu: true,
9557                 menuPosition: 'before'
9558         }, config );
9560         // Parent constructor
9561         OO.ui.MenuLayout.parent.call( this, config );
9563         /**
9564          * Menu DOM node
9565          *
9566          * @property {jQuery}
9567          */
9568         this.$menu = $( '<div>' );
9569         /**
9570          * Content DOM node
9571          *
9572          * @property {jQuery}
9573          */
9574         this.$content = $( '<div>' );
9576         // Initialization
9577         this.$menu
9578                 .addClass( 'oo-ui-menuLayout-menu' );
9579         this.$content.addClass( 'oo-ui-menuLayout-content' );
9580         this.$element
9581                 .addClass( 'oo-ui-menuLayout' )
9582                 .append( this.$content, this.$menu );
9583         this.setMenuPosition( config.menuPosition );
9584         this.toggleMenu( config.showMenu );
9587 /* Setup */
9589 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
9591 /* Methods */
9594  * Toggle menu.
9596  * @param {boolean} showMenu Show menu, omit to toggle
9597  * @chainable
9598  */
9599 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
9600         showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
9602         if ( this.showMenu !== showMenu ) {
9603                 this.showMenu = showMenu;
9604                 this.$element
9605                         .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
9606                         .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
9607         }
9609         return this;
9613  * Check if menu is visible
9615  * @return {boolean} Menu is visible
9616  */
9617 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
9618         return this.showMenu;
9622  * Set menu position.
9624  * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
9625  * @throws {Error} If position value is not supported
9626  * @chainable
9627  */
9628 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
9629         this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
9630         this.menuPosition = position;
9631         this.$element.addClass( 'oo-ui-menuLayout-' + position );
9633         return this;
9637  * Get menu position.
9639  * @return {string} Menu position
9640  */
9641 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
9642         return this.menuPosition;
9646  * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
9647  * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
9648  * through the pages and select which one to display. By default, only one page is
9649  * displayed at a time and the outline is hidden. When a user navigates to a new page,
9650  * the booklet layout automatically focuses on the first focusable element, unless the
9651  * default setting is changed. Optionally, booklets can be configured to show
9652  * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
9654  *     @example
9655  *     // Example of a BookletLayout that contains two PageLayouts.
9657  *     function PageOneLayout( name, config ) {
9658  *         PageOneLayout.parent.call( this, name, config );
9659  *         this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
9660  *     }
9661  *     OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
9662  *     PageOneLayout.prototype.setupOutlineItem = function () {
9663  *         this.outlineItem.setLabel( 'Page One' );
9664  *     };
9666  *     function PageTwoLayout( name, config ) {
9667  *         PageTwoLayout.parent.call( this, name, config );
9668  *         this.$element.append( '<p>Second page</p>' );
9669  *     }
9670  *     OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
9671  *     PageTwoLayout.prototype.setupOutlineItem = function () {
9672  *         this.outlineItem.setLabel( 'Page Two' );
9673  *     };
9675  *     var page1 = new PageOneLayout( 'one' ),
9676  *         page2 = new PageTwoLayout( 'two' );
9678  *     var booklet = new OO.ui.BookletLayout( {
9679  *         outlined: true
9680  *     } );
9682  *     booklet.addPages ( [ page1, page2 ] );
9683  *     $( 'body' ).append( booklet.$element );
9685  * @class
9686  * @extends OO.ui.MenuLayout
9688  * @constructor
9689  * @param {Object} [config] Configuration options
9690  * @cfg {boolean} [continuous=false] Show all pages, one after another
9691  * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
9692  * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
9693  * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
9694  */
9695 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
9696         // Configuration initialization
9697         config = config || {};
9699         // Parent constructor
9700         OO.ui.BookletLayout.parent.call( this, config );
9702         // Properties
9703         this.currentPageName = null;
9704         this.pages = {};
9705         this.ignoreFocus = false;
9706         this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9707         this.$content.append( this.stackLayout.$element );
9708         this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9709         this.outlineVisible = false;
9710         this.outlined = !!config.outlined;
9711         if ( this.outlined ) {
9712                 this.editable = !!config.editable;
9713                 this.outlineControlsWidget = null;
9714                 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
9715                 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
9716                 this.$menu.append( this.outlinePanel.$element );
9717                 this.outlineVisible = true;
9718                 if ( this.editable ) {
9719                         this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
9720                                 this.outlineSelectWidget
9721                         );
9722                 }
9723         }
9724         this.toggleMenu( this.outlined );
9726         // Events
9727         this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
9728         if ( this.outlined ) {
9729                 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
9730         }
9731         if ( this.autoFocus ) {
9732                 // Event 'focus' does not bubble, but 'focusin' does
9733                 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
9734         }
9736         // Initialization
9737         this.$element.addClass( 'oo-ui-bookletLayout' );
9738         this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
9739         if ( this.outlined ) {
9740                 this.outlinePanel.$element
9741                         .addClass( 'oo-ui-bookletLayout-outlinePanel' )
9742                         .append( this.outlineSelectWidget.$element );
9743                 if ( this.editable ) {
9744                         this.outlinePanel.$element
9745                                 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
9746                                 .append( this.outlineControlsWidget.$element );
9747                 }
9748         }
9751 /* Setup */
9753 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
9755 /* Events */
9758  * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
9759  * @event set
9760  * @param {OO.ui.PageLayout} page Current page
9761  */
9764  * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
9766  * @event add
9767  * @param {OO.ui.PageLayout[]} page Added pages
9768  * @param {number} index Index pages were added at
9769  */
9772  * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
9773  * {@link #removePages removed} from the booklet.
9775  * @event remove
9776  * @param {OO.ui.PageLayout[]} pages Removed pages
9777  */
9779 /* Methods */
9782  * Handle stack layout focus.
9784  * @private
9785  * @param {jQuery.Event} e Focusin event
9786  */
9787 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
9788         var name, $target;
9790         // Find the page that an element was focused within
9791         $target = $( e.target ).closest( '.oo-ui-pageLayout' );
9792         for ( name in this.pages ) {
9793                 // Check for page match, exclude current page to find only page changes
9794                 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
9795                         this.setPage( name );
9796                         break;
9797                 }
9798         }
9802  * Handle stack layout set events.
9804  * @private
9805  * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
9806  */
9807 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
9808         var layout = this;
9809         if ( page ) {
9810                 page.scrollElementIntoView( { complete: function () {
9811                         if ( layout.autoFocus ) {
9812                                 layout.focus();
9813                         }
9814                 } } );
9815         }
9819  * Focus the first input in the current page.
9821  * If no page is selected, the first selectable page will be selected.
9822  * If the focus is already in an element on the current page, nothing will happen.
9823  * @param {number} [itemIndex] A specific item to focus on
9824  */
9825 OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
9826         var page,
9827                 items = this.stackLayout.getItems();
9829         if ( itemIndex !== undefined && items[ itemIndex ] ) {
9830                 page = items[ itemIndex ];
9831         } else {
9832                 page = this.stackLayout.getCurrentItem();
9833         }
9835         if ( !page && this.outlined ) {
9836                 this.selectFirstSelectablePage();
9837                 page = this.stackLayout.getCurrentItem();
9838         }
9839         if ( !page ) {
9840                 return;
9841         }
9842         // Only change the focus if is not already in the current page
9843         if ( !OO.ui.contains( page.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
9844                 page.focus();
9845         }
9849  * Find the first focusable input in the booklet layout and focus
9850  * on it.
9851  */
9852 OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
9853         OO.ui.findFocusable( this.stackLayout.$element ).focus();
9857  * Handle outline widget select events.
9859  * @private
9860  * @param {OO.ui.OptionWidget|null} item Selected item
9861  */
9862 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
9863         if ( item ) {
9864                 this.setPage( item.getData() );
9865         }
9869  * Check if booklet has an outline.
9871  * @return {boolean} Booklet has an outline
9872  */
9873 OO.ui.BookletLayout.prototype.isOutlined = function () {
9874         return this.outlined;
9878  * Check if booklet has editing controls.
9880  * @return {boolean} Booklet is editable
9881  */
9882 OO.ui.BookletLayout.prototype.isEditable = function () {
9883         return this.editable;
9887  * Check if booklet has a visible outline.
9889  * @return {boolean} Outline is visible
9890  */
9891 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
9892         return this.outlined && this.outlineVisible;
9896  * Hide or show the outline.
9898  * @param {boolean} [show] Show outline, omit to invert current state
9899  * @chainable
9900  */
9901 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
9902         if ( this.outlined ) {
9903                 show = show === undefined ? !this.outlineVisible : !!show;
9904                 this.outlineVisible = show;
9905                 this.toggleMenu( show );
9906         }
9908         return this;
9912  * Get the page closest to the specified page.
9914  * @param {OO.ui.PageLayout} page Page to use as a reference point
9915  * @return {OO.ui.PageLayout|null} Page closest to the specified page
9916  */
9917 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
9918         var next, prev, level,
9919                 pages = this.stackLayout.getItems(),
9920                 index = pages.indexOf( page );
9922         if ( index !== -1 ) {
9923                 next = pages[ index + 1 ];
9924                 prev = pages[ index - 1 ];
9925                 // Prefer adjacent pages at the same level
9926                 if ( this.outlined ) {
9927                         level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
9928                         if (
9929                                 prev &&
9930                                 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
9931                         ) {
9932                                 return prev;
9933                         }
9934                         if (
9935                                 next &&
9936                                 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
9937                         ) {
9938                                 return next;
9939                         }
9940                 }
9941         }
9942         return prev || next || null;
9946  * Get the outline widget.
9948  * If the booklet is not outlined, the method will return `null`.
9950  * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
9951  */
9952 OO.ui.BookletLayout.prototype.getOutline = function () {
9953         return this.outlineSelectWidget;
9957  * Get the outline controls widget.
9959  * If the outline is not editable, the method will return `null`.
9961  * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
9962  */
9963 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
9964         return this.outlineControlsWidget;
9968  * Get a page by its symbolic name.
9970  * @param {string} name Symbolic name of page
9971  * @return {OO.ui.PageLayout|undefined} Page, if found
9972  */
9973 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
9974         return this.pages[ name ];
9978  * Get the current page.
9980  * @return {OO.ui.PageLayout|undefined} Current page, if found
9981  */
9982 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
9983         var name = this.getCurrentPageName();
9984         return name ? this.getPage( name ) : undefined;
9988  * Get the symbolic name of the current page.
9990  * @return {string|null} Symbolic name of the current page
9991  */
9992 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
9993         return this.currentPageName;
9997  * Add pages to the booklet layout
9999  * When pages are added with the same names as existing pages, the existing pages will be
10000  * automatically removed before the new pages are added.
10002  * @param {OO.ui.PageLayout[]} pages Pages to add
10003  * @param {number} index Index of the insertion point
10004  * @fires add
10005  * @chainable
10006  */
10007 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
10008         var i, len, name, page, item, currentIndex,
10009                 stackLayoutPages = this.stackLayout.getItems(),
10010                 remove = [],
10011                 items = [];
10013         // Remove pages with same names
10014         for ( i = 0, len = pages.length; i < len; i++ ) {
10015                 page = pages[ i ];
10016                 name = page.getName();
10018                 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
10019                         // Correct the insertion index
10020                         currentIndex = stackLayoutPages.indexOf( this.pages[ name ] );
10021                         if ( currentIndex !== -1 && currentIndex + 1 < index ) {
10022                                 index--;
10023                         }
10024                         remove.push( this.pages[ name ] );
10025                 }
10026         }
10027         if ( remove.length ) {
10028                 this.removePages( remove );
10029         }
10031         // Add new pages
10032         for ( i = 0, len = pages.length; i < len; i++ ) {
10033                 page = pages[ i ];
10034                 name = page.getName();
10035                 this.pages[ page.getName() ] = page;
10036                 if ( this.outlined ) {
10037                         item = new OO.ui.OutlineOptionWidget( { data: name } );
10038                         page.setOutlineItem( item );
10039                         items.push( item );
10040                 }
10041         }
10043         if ( this.outlined && items.length ) {
10044                 this.outlineSelectWidget.addItems( items, index );
10045                 this.selectFirstSelectablePage();
10046         }
10047         this.stackLayout.addItems( pages, index );
10048         this.emit( 'add', pages, index );
10050         return this;
10054  * Remove the specified pages from the booklet layout.
10056  * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
10058  * @param {OO.ui.PageLayout[]} pages An array of pages to remove
10059  * @fires remove
10060  * @chainable
10061  */
10062 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
10063         var i, len, name, page,
10064                 items = [];
10066         for ( i = 0, len = pages.length; i < len; i++ ) {
10067                 page = pages[ i ];
10068                 name = page.getName();
10069                 delete this.pages[ name ];
10070                 if ( this.outlined ) {
10071                         items.push( this.outlineSelectWidget.getItemFromData( name ) );
10072                         page.setOutlineItem( null );
10073                 }
10074         }
10075         if ( this.outlined && items.length ) {
10076                 this.outlineSelectWidget.removeItems( items );
10077                 this.selectFirstSelectablePage();
10078         }
10079         this.stackLayout.removeItems( pages );
10080         this.emit( 'remove', pages );
10082         return this;
10086  * Clear all pages from the booklet layout.
10088  * To remove only a subset of pages from the booklet, use the #removePages method.
10090  * @fires remove
10091  * @chainable
10092  */
10093 OO.ui.BookletLayout.prototype.clearPages = function () {
10094         var i, len,
10095                 pages = this.stackLayout.getItems();
10097         this.pages = {};
10098         this.currentPageName = null;
10099         if ( this.outlined ) {
10100                 this.outlineSelectWidget.clearItems();
10101                 for ( i = 0, len = pages.length; i < len; i++ ) {
10102                         pages[ i ].setOutlineItem( null );
10103                 }
10104         }
10105         this.stackLayout.clearItems();
10107         this.emit( 'remove', pages );
10109         return this;
10113  * Set the current page by symbolic name.
10115  * @fires set
10116  * @param {string} name Symbolic name of page
10117  */
10118 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
10119         var selectedItem,
10120                 $focused,
10121                 page = this.pages[ name ],
10122                 previousPage = this.currentPageName && this.pages[ this.currentPageName ];
10124         if ( name !== this.currentPageName ) {
10125                 if ( this.outlined ) {
10126                         selectedItem = this.outlineSelectWidget.getSelectedItem();
10127                         if ( selectedItem && selectedItem.getData() !== name ) {
10128                                 this.outlineSelectWidget.selectItemByData( name );
10129                         }
10130                 }
10131                 if ( page ) {
10132                         if ( previousPage ) {
10133                                 previousPage.setActive( false );
10134                                 // Blur anything focused if the next page doesn't have anything focusable.
10135                                 // This is not needed if the next page has something focusable (because once it is focused
10136                                 // this blur happens automatically). If the layout is non-continuous, this check is
10137                                 // meaningless because the next page is not visible yet and thus can't hold focus.
10138                                 if (
10139                                         this.autoFocus &&
10140                                         this.stackLayout.continuous &&
10141                                         OO.ui.findFocusable( page.$element ).length !== 0
10142                                 ) {
10143                                         $focused = previousPage.$element.find( ':focus' );
10144                                         if ( $focused.length ) {
10145                                                 $focused[ 0 ].blur();
10146                                         }
10147                                 }
10148                         }
10149                         this.currentPageName = name;
10150                         page.setActive( true );
10151                         this.stackLayout.setItem( page );
10152                         if ( !this.stackLayout.continuous && previousPage ) {
10153                                 // This should not be necessary, since any inputs on the previous page should have been
10154                                 // blurred when it was hidden, but browsers are not very consistent about this.
10155                                 $focused = previousPage.$element.find( ':focus' );
10156                                 if ( $focused.length ) {
10157                                         $focused[ 0 ].blur();
10158                                 }
10159                         }
10160                         this.emit( 'set', page );
10161                 }
10162         }
10166  * Select the first selectable page.
10168  * @chainable
10169  */
10170 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
10171         if ( !this.outlineSelectWidget.getSelectedItem() ) {
10172                 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
10173         }
10175         return this;
10179  * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
10180  * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
10181  * select which one to display. By default, only one card is displayed at a time. When a user
10182  * navigates to a new card, the index layout automatically focuses on the first focusable element,
10183  * unless the default setting is changed.
10185  * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
10187  *     @example
10188  *     // Example of a IndexLayout that contains two CardLayouts.
10190  *     function CardOneLayout( name, config ) {
10191  *         CardOneLayout.parent.call( this, name, config );
10192  *         this.$element.append( '<p>First card</p>' );
10193  *     }
10194  *     OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
10195  *     CardOneLayout.prototype.setupTabItem = function () {
10196  *         this.tabItem.setLabel( 'Card one' );
10197  *     };
10199  *     var card1 = new CardOneLayout( 'one' ),
10200  *         card2 = new CardLayout( 'two', { label: 'Card two' } );
10202  *     card2.$element.append( '<p>Second card</p>' );
10204  *     var index = new OO.ui.IndexLayout();
10206  *     index.addCards ( [ card1, card2 ] );
10207  *     $( 'body' ).append( index.$element );
10209  * @class
10210  * @extends OO.ui.MenuLayout
10212  * @constructor
10213  * @param {Object} [config] Configuration options
10214  * @cfg {boolean} [continuous=false] Show all cards, one after another
10215  * @cfg {boolean} [expanded=true] Expand the content panel to fill the entire parent element.
10216  * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
10217  */
10218 OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
10219         // Configuration initialization
10220         config = $.extend( {}, config, { menuPosition: 'top' } );
10222         // Parent constructor
10223         OO.ui.IndexLayout.parent.call( this, config );
10225         // Properties
10226         this.currentCardName = null;
10227         this.cards = {};
10228         this.ignoreFocus = false;
10229         this.stackLayout = new OO.ui.StackLayout( {
10230                 continuous: !!config.continuous,
10231                 expanded: config.expanded
10232         } );
10233         this.$content.append( this.stackLayout.$element );
10234         this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
10236         this.tabSelectWidget = new OO.ui.TabSelectWidget();
10237         this.tabPanel = new OO.ui.PanelLayout();
10238         this.$menu.append( this.tabPanel.$element );
10240         this.toggleMenu( true );
10242         // Events
10243         this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
10244         this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } );
10245         if ( this.autoFocus ) {
10246                 // Event 'focus' does not bubble, but 'focusin' does
10247                 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
10248         }
10250         // Initialization
10251         this.$element.addClass( 'oo-ui-indexLayout' );
10252         this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
10253         this.tabPanel.$element
10254                 .addClass( 'oo-ui-indexLayout-tabPanel' )
10255                 .append( this.tabSelectWidget.$element );
10258 /* Setup */
10260 OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
10262 /* Events */
10265  * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
10266  * @event set
10267  * @param {OO.ui.CardLayout} card Current card
10268  */
10271  * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
10273  * @event add
10274  * @param {OO.ui.CardLayout[]} card Added cards
10275  * @param {number} index Index cards were added at
10276  */
10279  * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
10280  * {@link #removeCards removed} from the index.
10282  * @event remove
10283  * @param {OO.ui.CardLayout[]} cards Removed cards
10284  */
10286 /* Methods */
10289  * Handle stack layout focus.
10291  * @private
10292  * @param {jQuery.Event} e Focusin event
10293  */
10294 OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
10295         var name, $target;
10297         // Find the card that an element was focused within
10298         $target = $( e.target ).closest( '.oo-ui-cardLayout' );
10299         for ( name in this.cards ) {
10300                 // Check for card match, exclude current card to find only card changes
10301                 if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
10302                         this.setCard( name );
10303                         break;
10304                 }
10305         }
10309  * Handle stack layout set events.
10311  * @private
10312  * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
10313  */
10314 OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
10315         var layout = this;
10316         if ( card ) {
10317                 card.scrollElementIntoView( { complete: function () {
10318                         if ( layout.autoFocus ) {
10319                                 layout.focus();
10320                         }
10321                 } } );
10322         }
10326  * Focus the first input in the current card.
10328  * If no card is selected, the first selectable card will be selected.
10329  * If the focus is already in an element on the current card, nothing will happen.
10330  * @param {number} [itemIndex] A specific item to focus on
10331  */
10332 OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
10333         var card,
10334                 items = this.stackLayout.getItems();
10336         if ( itemIndex !== undefined && items[ itemIndex ] ) {
10337                 card = items[ itemIndex ];
10338         } else {
10339                 card = this.stackLayout.getCurrentItem();
10340         }
10342         if ( !card ) {
10343                 this.selectFirstSelectableCard();
10344                 card = this.stackLayout.getCurrentItem();
10345         }
10346         if ( !card ) {
10347                 return;
10348         }
10349         // Only change the focus if is not already in the current page
10350         if ( !OO.ui.contains( card.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
10351                 card.focus();
10352         }
10356  * Find the first focusable input in the index layout and focus
10357  * on it.
10358  */
10359 OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
10360         OO.ui.findFocusable( this.stackLayout.$element ).focus();
10364  * Handle tab widget select events.
10366  * @private
10367  * @param {OO.ui.OptionWidget|null} item Selected item
10368  */
10369 OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
10370         if ( item ) {
10371                 this.setCard( item.getData() );
10372         }
10376  * Get the card closest to the specified card.
10378  * @param {OO.ui.CardLayout} card Card to use as a reference point
10379  * @return {OO.ui.CardLayout|null} Card closest to the specified card
10380  */
10381 OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
10382         var next, prev, level,
10383                 cards = this.stackLayout.getItems(),
10384                 index = cards.indexOf( card );
10386         if ( index !== -1 ) {
10387                 next = cards[ index + 1 ];
10388                 prev = cards[ index - 1 ];
10389                 // Prefer adjacent cards at the same level
10390                 level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
10391                 if (
10392                         prev &&
10393                         level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
10394                 ) {
10395                         return prev;
10396                 }
10397                 if (
10398                         next &&
10399                         level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel()
10400                 ) {
10401                         return next;
10402                 }
10403         }
10404         return prev || next || null;
10408  * Get the tabs widget.
10410  * @return {OO.ui.TabSelectWidget} Tabs widget
10411  */
10412 OO.ui.IndexLayout.prototype.getTabs = function () {
10413         return this.tabSelectWidget;
10417  * Get a card by its symbolic name.
10419  * @param {string} name Symbolic name of card
10420  * @return {OO.ui.CardLayout|undefined} Card, if found
10421  */
10422 OO.ui.IndexLayout.prototype.getCard = function ( name ) {
10423         return this.cards[ name ];
10427  * Get the current card.
10429  * @return {OO.ui.CardLayout|undefined} Current card, if found
10430  */
10431 OO.ui.IndexLayout.prototype.getCurrentCard = function () {
10432         var name = this.getCurrentCardName();
10433         return name ? this.getCard( name ) : undefined;
10437  * Get the symbolic name of the current card.
10439  * @return {string|null} Symbolic name of the current card
10440  */
10441 OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
10442         return this.currentCardName;
10446  * Add cards to the index layout
10448  * When cards are added with the same names as existing cards, the existing cards will be
10449  * automatically removed before the new cards are added.
10451  * @param {OO.ui.CardLayout[]} cards Cards to add
10452  * @param {number} index Index of the insertion point
10453  * @fires add
10454  * @chainable
10455  */
10456 OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
10457         var i, len, name, card, item, currentIndex,
10458                 stackLayoutCards = this.stackLayout.getItems(),
10459                 remove = [],
10460                 items = [];
10462         // Remove cards with same names
10463         for ( i = 0, len = cards.length; i < len; i++ ) {
10464                 card = cards[ i ];
10465                 name = card.getName();
10467                 if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
10468                         // Correct the insertion index
10469                         currentIndex = stackLayoutCards.indexOf( this.cards[ name ] );
10470                         if ( currentIndex !== -1 && currentIndex + 1 < index ) {
10471                                 index--;
10472                         }
10473                         remove.push( this.cards[ name ] );
10474                 }
10475         }
10476         if ( remove.length ) {
10477                 this.removeCards( remove );
10478         }
10480         // Add new cards
10481         for ( i = 0, len = cards.length; i < len; i++ ) {
10482                 card = cards[ i ];
10483                 name = card.getName();
10484                 this.cards[ card.getName() ] = card;
10485                 item = new OO.ui.TabOptionWidget( { data: name } );
10486                 card.setTabItem( item );
10487                 items.push( item );
10488         }
10490         if ( items.length ) {
10491                 this.tabSelectWidget.addItems( items, index );
10492                 this.selectFirstSelectableCard();
10493         }
10494         this.stackLayout.addItems( cards, index );
10495         this.emit( 'add', cards, index );
10497         return this;
10501  * Remove the specified cards from the index layout.
10503  * To remove all cards from the index, you may wish to use the #clearCards method instead.
10505  * @param {OO.ui.CardLayout[]} cards An array of cards to remove
10506  * @fires remove
10507  * @chainable
10508  */
10509 OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
10510         var i, len, name, card,
10511                 items = [];
10513         for ( i = 0, len = cards.length; i < len; i++ ) {
10514                 card = cards[ i ];
10515                 name = card.getName();
10516                 delete this.cards[ name ];
10517                 items.push( this.tabSelectWidget.getItemFromData( name ) );
10518                 card.setTabItem( null );
10519         }
10520         if ( items.length ) {
10521                 this.tabSelectWidget.removeItems( items );
10522                 this.selectFirstSelectableCard();
10523         }
10524         this.stackLayout.removeItems( cards );
10525         this.emit( 'remove', cards );
10527         return this;
10531  * Clear all cards from the index layout.
10533  * To remove only a subset of cards from the index, use the #removeCards method.
10535  * @fires remove
10536  * @chainable
10537  */
10538 OO.ui.IndexLayout.prototype.clearCards = function () {
10539         var i, len,
10540                 cards = this.stackLayout.getItems();
10542         this.cards = {};
10543         this.currentCardName = null;
10544         this.tabSelectWidget.clearItems();
10545         for ( i = 0, len = cards.length; i < len; i++ ) {
10546                 cards[ i ].setTabItem( null );
10547         }
10548         this.stackLayout.clearItems();
10550         this.emit( 'remove', cards );
10552         return this;
10556  * Set the current card by symbolic name.
10558  * @fires set
10559  * @param {string} name Symbolic name of card
10560  */
10561 OO.ui.IndexLayout.prototype.setCard = function ( name ) {
10562         var selectedItem,
10563                 $focused,
10564                 card = this.cards[ name ],
10565                 previousCard = this.currentCardName && this.cards[ this.currentCardName ];
10567         if ( name !== this.currentCardName ) {
10568                 selectedItem = this.tabSelectWidget.getSelectedItem();
10569                 if ( selectedItem && selectedItem.getData() !== name ) {
10570                         this.tabSelectWidget.selectItemByData( name );
10571                 }
10572                 if ( card ) {
10573                         if ( previousCard ) {
10574                                 previousCard.setActive( false );
10575                                 // Blur anything focused if the next card doesn't have anything focusable.
10576                                 // This is not needed if the next card has something focusable (because once it is focused
10577                                 // this blur happens automatically). If the layout is non-continuous, this check is
10578                                 // meaningless because the next card is not visible yet and thus can't hold focus.
10579                                 if (
10580                                         this.autoFocus &&
10581                                         this.stackLayout.continuous &&
10582                                         OO.ui.findFocusable( card.$element ).length !== 0
10583                                 ) {
10584                                         $focused = previousCard.$element.find( ':focus' );
10585                                         if ( $focused.length ) {
10586                                                 $focused[ 0 ].blur();
10587                                         }
10588                                 }
10589                         }
10590                         this.currentCardName = name;
10591                         card.setActive( true );
10592                         this.stackLayout.setItem( card );
10593                         if ( !this.stackLayout.continuous && previousCard ) {
10594                                 // This should not be necessary, since any inputs on the previous card should have been
10595                                 // blurred when it was hidden, but browsers are not very consistent about this.
10596                                 $focused = previousCard.$element.find( ':focus' );
10597                                 if ( $focused.length ) {
10598                                         $focused[ 0 ].blur();
10599                                 }
10600                         }
10601                         this.emit( 'set', card );
10602                 }
10603         }
10607  * Select the first selectable card.
10609  * @chainable
10610  */
10611 OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
10612         if ( !this.tabSelectWidget.getSelectedItem() ) {
10613                 this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
10614         }
10616         return this;
10620  * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10621  * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10623  *     @example
10624  *     // Example of a panel layout
10625  *     var panel = new OO.ui.PanelLayout( {
10626  *         expanded: false,
10627  *         framed: true,
10628  *         padded: true,
10629  *         $content: $( '<p>A panel layout with padding and a frame.</p>' )
10630  *     } );
10631  *     $( 'body' ).append( panel.$element );
10633  * @class
10634  * @extends OO.ui.Layout
10636  * @constructor
10637  * @param {Object} [config] Configuration options
10638  * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10639  * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10640  * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10641  * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10642  */
10643 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
10644         // Configuration initialization
10645         config = $.extend( {
10646                 scrollable: false,
10647                 padded: false,
10648                 expanded: true,
10649                 framed: false
10650         }, config );
10652         // Parent constructor
10653         OO.ui.PanelLayout.parent.call( this, config );
10655         // Initialization
10656         this.$element.addClass( 'oo-ui-panelLayout' );
10657         if ( config.scrollable ) {
10658                 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
10659         }
10660         if ( config.padded ) {
10661                 this.$element.addClass( 'oo-ui-panelLayout-padded' );
10662         }
10663         if ( config.expanded ) {
10664                 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
10665         }
10666         if ( config.framed ) {
10667                 this.$element.addClass( 'oo-ui-panelLayout-framed' );
10668         }
10671 /* Setup */
10673 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
10675 /* Methods */
10678  * Focus the panel layout
10680  * The default implementation just focuses the first focusable element in the panel
10681  */
10682 OO.ui.PanelLayout.prototype.focus = function () {
10683         OO.ui.findFocusable( this.$element ).focus();
10687  * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
10688  * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
10689  * rather extended to include the required content and functionality.
10691  * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
10692  * item is customized (with a label) using the #setupTabItem method. See
10693  * {@link OO.ui.IndexLayout IndexLayout} for an example.
10695  * @class
10696  * @extends OO.ui.PanelLayout
10698  * @constructor
10699  * @param {string} name Unique symbolic name of card
10700  * @param {Object} [config] Configuration options
10701  * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for card's tab
10702  */
10703 OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
10704         // Allow passing positional parameters inside the config object
10705         if ( OO.isPlainObject( name ) && config === undefined ) {
10706                 config = name;
10707                 name = config.name;
10708         }
10710         // Configuration initialization
10711         config = $.extend( { scrollable: true }, config );
10713         // Parent constructor
10714         OO.ui.CardLayout.parent.call( this, config );
10716         // Properties
10717         this.name = name;
10718         this.label = config.label;
10719         this.tabItem = null;
10720         this.active = false;
10722         // Initialization
10723         this.$element.addClass( 'oo-ui-cardLayout' );
10726 /* Setup */
10728 OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
10730 /* Events */
10733  * An 'active' event is emitted when the card becomes active. Cards become active when they are
10734  * shown in a index layout that is configured to display only one card at a time.
10736  * @event active
10737  * @param {boolean} active Card is active
10738  */
10740 /* Methods */
10743  * Get the symbolic name of the card.
10745  * @return {string} Symbolic name of card
10746  */
10747 OO.ui.CardLayout.prototype.getName = function () {
10748         return this.name;
10752  * Check if card is active.
10754  * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
10755  * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
10757  * @return {boolean} Card is active
10758  */
10759 OO.ui.CardLayout.prototype.isActive = function () {
10760         return this.active;
10764  * Get tab item.
10766  * The tab item allows users to access the card from the index's tab
10767  * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
10769  * @return {OO.ui.TabOptionWidget|null} Tab option widget
10770  */
10771 OO.ui.CardLayout.prototype.getTabItem = function () {
10772         return this.tabItem;
10776  * Set or unset the tab item.
10778  * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
10779  * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
10780  * level), use #setupTabItem instead of this method.
10782  * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
10783  * @chainable
10784  */
10785 OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
10786         this.tabItem = tabItem || null;
10787         if ( tabItem ) {
10788                 this.setupTabItem();
10789         }
10790         return this;
10794  * Set up the tab item.
10796  * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
10797  * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
10798  * the #setTabItem method instead.
10800  * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
10801  * @chainable
10802  */
10803 OO.ui.CardLayout.prototype.setupTabItem = function () {
10804         if ( this.label ) {
10805                 this.tabItem.setLabel( this.label );
10806         }
10807         return this;
10811  * Set the card to its 'active' state.
10813  * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
10814  * CSS is applied to the tab item to reflect the card's active state. Outside of the index
10815  * context, setting the active state on a card does nothing.
10817  * @param {boolean} value Card is active
10818  * @fires active
10819  */
10820 OO.ui.CardLayout.prototype.setActive = function ( active ) {
10821         active = !!active;
10823         if ( active !== this.active ) {
10824                 this.active = active;
10825                 this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
10826                 this.emit( 'active', this.active );
10827         }
10831  * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
10832  * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
10833  * rather extended to include the required content and functionality.
10835  * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
10836  * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
10837  * {@link OO.ui.BookletLayout BookletLayout} for an example.
10839  * @class
10840  * @extends OO.ui.PanelLayout
10842  * @constructor
10843  * @param {string} name Unique symbolic name of page
10844  * @param {Object} [config] Configuration options
10845  */
10846 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
10847         // Allow passing positional parameters inside the config object
10848         if ( OO.isPlainObject( name ) && config === undefined ) {
10849                 config = name;
10850                 name = config.name;
10851         }
10853         // Configuration initialization
10854         config = $.extend( { scrollable: true }, config );
10856         // Parent constructor
10857         OO.ui.PageLayout.parent.call( this, config );
10859         // Properties
10860         this.name = name;
10861         this.outlineItem = null;
10862         this.active = false;
10864         // Initialization
10865         this.$element.addClass( 'oo-ui-pageLayout' );
10868 /* Setup */
10870 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
10872 /* Events */
10875  * An 'active' event is emitted when the page becomes active. Pages become active when they are
10876  * shown in a booklet layout that is configured to display only one page at a time.
10878  * @event active
10879  * @param {boolean} active Page is active
10880  */
10882 /* Methods */
10885  * Get the symbolic name of the page.
10887  * @return {string} Symbolic name of page
10888  */
10889 OO.ui.PageLayout.prototype.getName = function () {
10890         return this.name;
10894  * Check if page is active.
10896  * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
10897  * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
10899  * @return {boolean} Page is active
10900  */
10901 OO.ui.PageLayout.prototype.isActive = function () {
10902         return this.active;
10906  * Get outline item.
10908  * The outline item allows users to access the page from the booklet's outline
10909  * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
10911  * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
10912  */
10913 OO.ui.PageLayout.prototype.getOutlineItem = function () {
10914         return this.outlineItem;
10918  * Set or unset the outline item.
10920  * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
10921  * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
10922  * level), use #setupOutlineItem instead of this method.
10924  * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
10925  * @chainable
10926  */
10927 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
10928         this.outlineItem = outlineItem || null;
10929         if ( outlineItem ) {
10930                 this.setupOutlineItem();
10931         }
10932         return this;
10936  * Set up the outline item.
10938  * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
10939  * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
10940  * the #setOutlineItem method instead.
10942  * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
10943  * @chainable
10944  */
10945 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
10946         return this;
10950  * Set the page to its 'active' state.
10952  * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
10953  * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
10954  * context, setting the active state on a page does nothing.
10956  * @param {boolean} value Page is active
10957  * @fires active
10958  */
10959 OO.ui.PageLayout.prototype.setActive = function ( active ) {
10960         active = !!active;
10962         if ( active !== this.active ) {
10963                 this.active = active;
10964                 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
10965                 this.emit( 'active', this.active );
10966         }
10970  * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
10971  * at a time, though the stack layout can also be configured to show all contained panels, one after another,
10972  * by setting the #continuous option to 'true'.
10974  *     @example
10975  *     // A stack layout with two panels, configured to be displayed continously
10976  *     var myStack = new OO.ui.StackLayout( {
10977  *         items: [
10978  *             new OO.ui.PanelLayout( {
10979  *                 $content: $( '<p>Panel One</p>' ),
10980  *                 padded: true,
10981  *                 framed: true
10982  *             } ),
10983  *             new OO.ui.PanelLayout( {
10984  *                 $content: $( '<p>Panel Two</p>' ),
10985  *                 padded: true,
10986  *                 framed: true
10987  *             } )
10988  *         ],
10989  *         continuous: true
10990  *     } );
10991  *     $( 'body' ).append( myStack.$element );
10993  * @class
10994  * @extends OO.ui.PanelLayout
10995  * @mixins OO.ui.mixin.GroupElement
10997  * @constructor
10998  * @param {Object} [config] Configuration options
10999  * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
11000  * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
11001  */
11002 OO.ui.StackLayout = function OoUiStackLayout( config ) {
11003         // Configuration initialization
11004         config = $.extend( { scrollable: true }, config );
11006         // Parent constructor
11007         OO.ui.StackLayout.parent.call( this, config );
11009         // Mixin constructors
11010         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11012         // Properties
11013         this.currentItem = null;
11014         this.continuous = !!config.continuous;
11016         // Initialization
11017         this.$element.addClass( 'oo-ui-stackLayout' );
11018         if ( this.continuous ) {
11019                 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
11020         }
11021         if ( Array.isArray( config.items ) ) {
11022                 this.addItems( config.items );
11023         }
11026 /* Setup */
11028 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
11029 OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
11031 /* Events */
11034  * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
11035  * {@link #clearItems cleared} or {@link #setItem displayed}.
11037  * @event set
11038  * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
11039  */
11041 /* Methods */
11044  * Get the current panel.
11046  * @return {OO.ui.Layout|null}
11047  */
11048 OO.ui.StackLayout.prototype.getCurrentItem = function () {
11049         return this.currentItem;
11053  * Unset the current item.
11055  * @private
11056  * @param {OO.ui.StackLayout} layout
11057  * @fires set
11058  */
11059 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
11060         var prevItem = this.currentItem;
11061         if ( prevItem === null ) {
11062                 return;
11063         }
11065         this.currentItem = null;
11066         this.emit( 'set', null );
11070  * Add panel layouts to the stack layout.
11072  * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
11073  * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
11074  * by the index.
11076  * @param {OO.ui.Layout[]} items Panels to add
11077  * @param {number} [index] Index of the insertion point
11078  * @chainable
11079  */
11080 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
11081         // Update the visibility
11082         this.updateHiddenState( items, this.currentItem );
11084         // Mixin method
11085         OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
11087         if ( !this.currentItem && items.length ) {
11088                 this.setItem( items[ 0 ] );
11089         }
11091         return this;
11095  * Remove the specified panels from the stack layout.
11097  * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
11098  * you may wish to use the #clearItems method instead.
11100  * @param {OO.ui.Layout[]} items Panels to remove
11101  * @chainable
11102  * @fires set
11103  */
11104 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
11105         // Mixin method
11106         OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
11108         if ( items.indexOf( this.currentItem ) !== -1 ) {
11109                 if ( this.items.length ) {
11110                         this.setItem( this.items[ 0 ] );
11111                 } else {
11112                         this.unsetCurrentItem();
11113                 }
11114         }
11116         return this;
11120  * Clear all panels from the stack layout.
11122  * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
11123  * a subset of panels, use the #removeItems method.
11125  * @chainable
11126  * @fires set
11127  */
11128 OO.ui.StackLayout.prototype.clearItems = function () {
11129         this.unsetCurrentItem();
11130         OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
11132         return this;
11136  * Show the specified panel.
11138  * If another panel is currently displayed, it will be hidden.
11140  * @param {OO.ui.Layout} item Panel to show
11141  * @chainable
11142  * @fires set
11143  */
11144 OO.ui.StackLayout.prototype.setItem = function ( item ) {
11145         if ( item !== this.currentItem ) {
11146                 this.updateHiddenState( this.items, item );
11148                 if ( this.items.indexOf( item ) !== -1 ) {
11149                         this.currentItem = item;
11150                         this.emit( 'set', item );
11151                 } else {
11152                         this.unsetCurrentItem();
11153                 }
11154         }
11156         return this;
11160  * Update the visibility of all items in case of non-continuous view.
11162  * Ensure all items are hidden except for the selected one.
11163  * This method does nothing when the stack is continuous.
11165  * @private
11166  * @param {OO.ui.Layout[]} items Item list iterate over
11167  * @param {OO.ui.Layout} [selectedItem] Selected item to show
11168  */
11169 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
11170         var i, len;
11172         if ( !this.continuous ) {
11173                 for ( i = 0, len = items.length; i < len; i++ ) {
11174                         if ( !selectedItem || selectedItem !== items[ i ] ) {
11175                                 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
11176                         }
11177                 }
11178                 if ( selectedItem ) {
11179                         selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
11180                 }
11181         }
11185  * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
11186  * items), with small margins between them. Convenient when you need to put a number of block-level
11187  * widgets on a single line next to each other.
11189  * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
11191  *     @example
11192  *     // HorizontalLayout with a text input and a label
11193  *     var layout = new OO.ui.HorizontalLayout( {
11194  *       items: [
11195  *         new OO.ui.LabelWidget( { label: 'Label' } ),
11196  *         new OO.ui.TextInputWidget( { value: 'Text' } )
11197  *       ]
11198  *     } );
11199  *     $( 'body' ).append( layout.$element );
11201  * @class
11202  * @extends OO.ui.Layout
11203  * @mixins OO.ui.mixin.GroupElement
11205  * @constructor
11206  * @param {Object} [config] Configuration options
11207  * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
11208  */
11209 OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
11210         // Configuration initialization
11211         config = config || {};
11213         // Parent constructor
11214         OO.ui.HorizontalLayout.parent.call( this, config );
11216         // Mixin constructors
11217         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11219         // Initialization
11220         this.$element.addClass( 'oo-ui-horizontalLayout' );
11221         if ( Array.isArray( config.items ) ) {
11222                 this.addItems( config.items );
11223         }
11226 /* Setup */
11228 OO.inheritClass( OO.ui.HorizontalLayout, OO.ui.Layout );
11229 OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
11232  * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11233  * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
11234  * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
11235  * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
11236  * the tool.
11238  * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
11239  * set up.
11241  *     @example
11242  *     // Example of a BarToolGroup with two tools
11243  *     var toolFactory = new OO.ui.ToolFactory();
11244  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
11245  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11247  *     // We will be placing status text in this element when tools are used
11248  *     var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
11250  *     // Define the tools that we're going to place in our toolbar
11252  *     // Create a class inheriting from OO.ui.Tool
11253  *     function PictureTool() {
11254  *         PictureTool.parent.apply( this, arguments );
11255  *     }
11256  *     OO.inheritClass( PictureTool, OO.ui.Tool );
11257  *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
11258  *     // of 'icon' and 'title' (displayed icon and text).
11259  *     PictureTool.static.name = 'picture';
11260  *     PictureTool.static.icon = 'picture';
11261  *     PictureTool.static.title = 'Insert picture';
11262  *     // Defines the action that will happen when this tool is selected (clicked).
11263  *     PictureTool.prototype.onSelect = function () {
11264  *         $area.text( 'Picture tool clicked!' );
11265  *         // Never display this tool as "active" (selected).
11266  *         this.setActive( false );
11267  *     };
11268  *     // Make this tool available in our toolFactory and thus our toolbar
11269  *     toolFactory.register( PictureTool );
11271  *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
11272  *     // little popup window (a PopupWidget).
11273  *     function HelpTool( toolGroup, config ) {
11274  *         OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
11275  *             padded: true,
11276  *             label: 'Help',
11277  *             head: true
11278  *         } }, config ) );
11279  *         this.popup.$body.append( '<p>I am helpful!</p>' );
11280  *     }
11281  *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
11282  *     HelpTool.static.name = 'help';
11283  *     HelpTool.static.icon = 'help';
11284  *     HelpTool.static.title = 'Help';
11285  *     toolFactory.register( HelpTool );
11287  *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
11288  *     // used once (but not all defined tools must be used).
11289  *     toolbar.setup( [
11290  *         {
11291  *             // 'bar' tool groups display tools by icon only
11292  *             type: 'bar',
11293  *             include: [ 'picture', 'help' ]
11294  *         }
11295  *     ] );
11297  *     // Create some UI around the toolbar and place it in the document
11298  *     var frame = new OO.ui.PanelLayout( {
11299  *         expanded: false,
11300  *         framed: true
11301  *     } );
11302  *     var contentFrame = new OO.ui.PanelLayout( {
11303  *         expanded: false,
11304  *         padded: true
11305  *     } );
11306  *     frame.$element.append(
11307  *         toolbar.$element,
11308  *         contentFrame.$element.append( $area )
11309  *     );
11310  *     $( 'body' ).append( frame.$element );
11312  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
11313  *     // document.
11314  *     toolbar.initialize();
11316  * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
11317  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
11319  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11321  * @class
11322  * @extends OO.ui.ToolGroup
11324  * @constructor
11325  * @param {OO.ui.Toolbar} toolbar
11326  * @param {Object} [config] Configuration options
11327  */
11328 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
11329         // Allow passing positional parameters inside the config object
11330         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11331                 config = toolbar;
11332                 toolbar = config.toolbar;
11333         }
11335         // Parent constructor
11336         OO.ui.BarToolGroup.parent.call( this, toolbar, config );
11338         // Initialization
11339         this.$element.addClass( 'oo-ui-barToolGroup' );
11342 /* Setup */
11344 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
11346 /* Static Properties */
11348 OO.ui.BarToolGroup.static.titleTooltips = true;
11350 OO.ui.BarToolGroup.static.accelTooltips = true;
11352 OO.ui.BarToolGroup.static.name = 'bar';
11355  * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
11356  * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
11357  * optional icon and label. This class can be used for other base classes that also use this functionality.
11359  * @abstract
11360  * @class
11361  * @extends OO.ui.ToolGroup
11362  * @mixins OO.ui.mixin.IconElement
11363  * @mixins OO.ui.mixin.IndicatorElement
11364  * @mixins OO.ui.mixin.LabelElement
11365  * @mixins OO.ui.mixin.TitledElement
11366  * @mixins OO.ui.mixin.ClippableElement
11367  * @mixins OO.ui.mixin.TabIndexedElement
11369  * @constructor
11370  * @param {OO.ui.Toolbar} toolbar
11371  * @param {Object} [config] Configuration options
11372  * @cfg {string} [header] Text to display at the top of the popup
11373  */
11374 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
11375         // Allow passing positional parameters inside the config object
11376         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11377                 config = toolbar;
11378                 toolbar = config.toolbar;
11379         }
11381         // Configuration initialization
11382         config = config || {};
11384         // Parent constructor
11385         OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
11387         // Properties
11388         this.active = false;
11389         this.dragging = false;
11390         this.onBlurHandler = this.onBlur.bind( this );
11391         this.$handle = $( '<span>' );
11393         // Mixin constructors
11394         OO.ui.mixin.IconElement.call( this, config );
11395         OO.ui.mixin.IndicatorElement.call( this, config );
11396         OO.ui.mixin.LabelElement.call( this, config );
11397         OO.ui.mixin.TitledElement.call( this, config );
11398         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
11399         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
11401         // Events
11402         this.$handle.on( {
11403                 keydown: this.onHandleMouseKeyDown.bind( this ),
11404                 keyup: this.onHandleMouseKeyUp.bind( this ),
11405                 mousedown: this.onHandleMouseKeyDown.bind( this ),
11406                 mouseup: this.onHandleMouseKeyUp.bind( this )
11407         } );
11409         // Initialization
11410         this.$handle
11411                 .addClass( 'oo-ui-popupToolGroup-handle' )
11412                 .append( this.$icon, this.$label, this.$indicator );
11413         // If the pop-up should have a header, add it to the top of the toolGroup.
11414         // Note: If this feature is useful for other widgets, we could abstract it into an
11415         // OO.ui.HeaderedElement mixin constructor.
11416         if ( config.header !== undefined ) {
11417                 this.$group
11418                         .prepend( $( '<span>' )
11419                                 .addClass( 'oo-ui-popupToolGroup-header' )
11420                                 .text( config.header )
11421                         );
11422         }
11423         this.$element
11424                 .addClass( 'oo-ui-popupToolGroup' )
11425                 .prepend( this.$handle );
11428 /* Setup */
11430 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
11431 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
11432 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
11433 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
11434 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
11435 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
11436 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
11438 /* Methods */
11441  * @inheritdoc
11442  */
11443 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
11444         // Parent method
11445         OO.ui.PopupToolGroup.parent.prototype.setDisabled.apply( this, arguments );
11447         if ( this.isDisabled() && this.isElementAttached() ) {
11448                 this.setActive( false );
11449         }
11453  * Handle focus being lost.
11455  * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
11457  * @protected
11458  * @param {jQuery.Event} e Mouse up or key up event
11459  */
11460 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
11461         // Only deactivate when clicking outside the dropdown element
11462         if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
11463                 this.setActive( false );
11464         }
11468  * @inheritdoc
11469  */
11470 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
11471         // Only close toolgroup when a tool was actually selected
11472         if (
11473                 !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
11474                 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11475         ) {
11476                 this.setActive( false );
11477         }
11478         return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
11482  * Handle mouse up and key up events.
11484  * @protected
11485  * @param {jQuery.Event} e Mouse up or key up event
11486  */
11487 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
11488         if (
11489                 !this.isDisabled() &&
11490                 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11491         ) {
11492                 return false;
11493         }
11497  * Handle mouse down and key down events.
11499  * @protected
11500  * @param {jQuery.Event} e Mouse down or key down event
11501  */
11502 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
11503         if (
11504                 !this.isDisabled() &&
11505                 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11506         ) {
11507                 this.setActive( !this.active );
11508                 return false;
11509         }
11513  * Switch into 'active' mode.
11515  * When active, the popup is visible. A mouseup event anywhere in the document will trigger
11516  * deactivation.
11517  */
11518 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
11519         var containerWidth, containerLeft;
11520         value = !!value;
11521         if ( this.active !== value ) {
11522                 this.active = value;
11523                 if ( value ) {
11524                         OO.ui.addCaptureEventListener( this.getElementDocument(), 'mouseup', this.onBlurHandler );
11525                         OO.ui.addCaptureEventListener( this.getElementDocument(), 'keyup', this.onBlurHandler );
11527                         this.$clippable.css( 'left', '' );
11528                         // Try anchoring the popup to the left first
11529                         this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
11530                         this.toggleClipping( true );
11531                         if ( this.isClippedHorizontally() ) {
11532                                 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
11533                                 this.toggleClipping( false );
11534                                 this.$element
11535                                         .removeClass( 'oo-ui-popupToolGroup-left' )
11536                                         .addClass( 'oo-ui-popupToolGroup-right' );
11537                                 this.toggleClipping( true );
11538                         }
11539                         if ( this.isClippedHorizontally() ) {
11540                                 // Anchoring to the right also caused the popup to clip, so just make it fill the container
11541                                 containerWidth = this.$clippableScrollableContainer.width();
11542                                 containerLeft = this.$clippableScrollableContainer.offset().left;
11544                                 this.toggleClipping( false );
11545                                 this.$element.removeClass( 'oo-ui-popupToolGroup-right' );
11547                                 this.$clippable.css( {
11548                                         left: -( this.$element.offset().left - containerLeft ),
11549                                         width: containerWidth
11550                                 } );
11551                         }
11552                 } else {
11553                         OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup', this.onBlurHandler );
11554                         OO.ui.removeCaptureEventListener( this.getElementDocument(), 'keyup', this.onBlurHandler );
11555                         this.$element.removeClass(
11556                                 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left  oo-ui-popupToolGroup-right'
11557                         );
11558                         this.toggleClipping( false );
11559                 }
11560         }
11564  * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11565  * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
11566  * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
11567  * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
11568  * with a label, icon, indicator, header, and title.
11570  * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
11571  * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
11572  * users to collapse the list again.
11574  * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
11575  * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
11576  * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
11578  *     @example
11579  *     // Example of a ListToolGroup
11580  *     var toolFactory = new OO.ui.ToolFactory();
11581  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
11582  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11584  *     // Configure and register two tools
11585  *     function SettingsTool() {
11586  *         SettingsTool.parent.apply( this, arguments );
11587  *     }
11588  *     OO.inheritClass( SettingsTool, OO.ui.Tool );
11589  *     SettingsTool.static.name = 'settings';
11590  *     SettingsTool.static.icon = 'settings';
11591  *     SettingsTool.static.title = 'Change settings';
11592  *     SettingsTool.prototype.onSelect = function () {
11593  *         this.setActive( false );
11594  *     };
11595  *     toolFactory.register( SettingsTool );
11596  *     // Register two more tools, nothing interesting here
11597  *     function StuffTool() {
11598  *         StuffTool.parent.apply( this, arguments );
11599  *     }
11600  *     OO.inheritClass( StuffTool, OO.ui.Tool );
11601  *     StuffTool.static.name = 'stuff';
11602  *     StuffTool.static.icon = 'ellipsis';
11603  *     StuffTool.static.title = 'Change the world';
11604  *     StuffTool.prototype.onSelect = function () {
11605  *         this.setActive( false );
11606  *     };
11607  *     toolFactory.register( StuffTool );
11608  *     toolbar.setup( [
11609  *         {
11610  *             // Configurations for list toolgroup.
11611  *             type: 'list',
11612  *             label: 'ListToolGroup',
11613  *             indicator: 'down',
11614  *             icon: 'picture',
11615  *             title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
11616  *             header: 'This is the header',
11617  *             include: [ 'settings', 'stuff' ],
11618  *             allowCollapse: ['stuff']
11619  *         }
11620  *     ] );
11622  *     // Create some UI around the toolbar and place it in the document
11623  *     var frame = new OO.ui.PanelLayout( {
11624  *         expanded: false,
11625  *         framed: true
11626  *     } );
11627  *     frame.$element.append(
11628  *         toolbar.$element
11629  *     );
11630  *     $( 'body' ).append( frame.$element );
11631  *     // Build the toolbar. This must be done after the toolbar has been appended to the document.
11632  *     toolbar.initialize();
11634  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
11636  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11638  * @class
11639  * @extends OO.ui.PopupToolGroup
11641  * @constructor
11642  * @param {OO.ui.Toolbar} toolbar
11643  * @param {Object} [config] Configuration options
11644  * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
11645  *  will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
11646  *  the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
11647  *  are included in the toolgroup, but are not designated as collapsible, will always be displayed.
11648  *  To open a collapsible list in its expanded state, set #expanded to 'true'.
11649  * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
11650  *  Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
11651  * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
11652  *  been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
11653  *  when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
11654  */
11655 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
11656         // Allow passing positional parameters inside the config object
11657         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11658                 config = toolbar;
11659                 toolbar = config.toolbar;
11660         }
11662         // Configuration initialization
11663         config = config || {};
11665         // Properties (must be set before parent constructor, which calls #populate)
11666         this.allowCollapse = config.allowCollapse;
11667         this.forceExpand = config.forceExpand;
11668         this.expanded = config.expanded !== undefined ? config.expanded : false;
11669         this.collapsibleTools = [];
11671         // Parent constructor
11672         OO.ui.ListToolGroup.parent.call( this, toolbar, config );
11674         // Initialization
11675         this.$element.addClass( 'oo-ui-listToolGroup' );
11678 /* Setup */
11680 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
11682 /* Static Properties */
11684 OO.ui.ListToolGroup.static.name = 'list';
11686 /* Methods */
11689  * @inheritdoc
11690  */
11691 OO.ui.ListToolGroup.prototype.populate = function () {
11692         var i, len, allowCollapse = [];
11694         OO.ui.ListToolGroup.parent.prototype.populate.call( this );
11696         // Update the list of collapsible tools
11697         if ( this.allowCollapse !== undefined ) {
11698                 allowCollapse = this.allowCollapse;
11699         } else if ( this.forceExpand !== undefined ) {
11700                 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
11701         }
11703         this.collapsibleTools = [];
11704         for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
11705                 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
11706                         this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
11707                 }
11708         }
11710         // Keep at the end, even when tools are added
11711         this.$group.append( this.getExpandCollapseTool().$element );
11713         this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
11714         this.updateCollapsibleState();
11717 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
11718         var ExpandCollapseTool;
11719         if ( this.expandCollapseTool === undefined ) {
11720                 ExpandCollapseTool = function () {
11721                         ExpandCollapseTool.parent.apply( this, arguments );
11722                 };
11724                 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
11726                 ExpandCollapseTool.prototype.onSelect = function () {
11727                         this.toolGroup.expanded = !this.toolGroup.expanded;
11728                         this.toolGroup.updateCollapsibleState();
11729                         this.setActive( false );
11730                 };
11731                 ExpandCollapseTool.prototype.onUpdateState = function () {
11732                         // Do nothing. Tool interface requires an implementation of this function.
11733                 };
11735                 ExpandCollapseTool.static.name = 'more-fewer';
11737                 this.expandCollapseTool = new ExpandCollapseTool( this );
11738         }
11739         return this.expandCollapseTool;
11743  * @inheritdoc
11744  */
11745 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
11746         // Do not close the popup when the user wants to show more/fewer tools
11747         if (
11748                 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
11749                 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11750         ) {
11751                 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
11752                 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
11753                 return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
11754         } else {
11755                 return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
11756         }
11759 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
11760         var i, len;
11762         this.getExpandCollapseTool()
11763                 .setIcon( this.expanded ? 'collapse' : 'expand' )
11764                 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
11766         for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
11767                 this.collapsibleTools[ i ].toggle( this.expanded );
11768         }
11772  * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11773  * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
11774  * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
11775  * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
11776  * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
11777  * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
11779  * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
11780  * is set up. Note that all tools must define an {@link OO.ui.Tool#onUpdateState onUpdateState} method if
11781  * a MenuToolGroup is used.
11783  *     @example
11784  *     // Example of a MenuToolGroup
11785  *     var toolFactory = new OO.ui.ToolFactory();
11786  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
11787  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11789  *     // We will be placing status text in this element when tools are used
11790  *     var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
11792  *     // Define the tools that we're going to place in our toolbar
11794  *     function SettingsTool() {
11795  *         SettingsTool.parent.apply( this, arguments );
11796  *         this.reallyActive = false;
11797  *     }
11798  *     OO.inheritClass( SettingsTool, OO.ui.Tool );
11799  *     SettingsTool.static.name = 'settings';
11800  *     SettingsTool.static.icon = 'settings';
11801  *     SettingsTool.static.title = 'Change settings';
11802  *     SettingsTool.prototype.onSelect = function () {
11803  *         $area.text( 'Settings tool clicked!' );
11804  *         // Toggle the active state on each click
11805  *         this.reallyActive = !this.reallyActive;
11806  *         this.setActive( this.reallyActive );
11807  *         // To update the menu label
11808  *         this.toolbar.emit( 'updateState' );
11809  *     };
11810  *     SettingsTool.prototype.onUpdateState = function () {
11811  *     };
11812  *     toolFactory.register( SettingsTool );
11814  *     function StuffTool() {
11815  *         StuffTool.parent.apply( this, arguments );
11816  *         this.reallyActive = false;
11817  *     }
11818  *     OO.inheritClass( StuffTool, OO.ui.Tool );
11819  *     StuffTool.static.name = 'stuff';
11820  *     StuffTool.static.icon = 'ellipsis';
11821  *     StuffTool.static.title = 'More stuff';
11822  *     StuffTool.prototype.onSelect = function () {
11823  *         $area.text( 'More stuff tool clicked!' );
11824  *         // Toggle the active state on each click
11825  *         this.reallyActive = !this.reallyActive;
11826  *         this.setActive( this.reallyActive );
11827  *         // To update the menu label
11828  *         this.toolbar.emit( 'updateState' );
11829  *     };
11830  *     StuffTool.prototype.onUpdateState = function () {
11831  *     };
11832  *     toolFactory.register( StuffTool );
11834  *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
11835  *     // used once (but not all defined tools must be used).
11836  *     toolbar.setup( [
11837  *         {
11838  *             type: 'menu',
11839  *             header: 'This is the (optional) header',
11840  *             title: 'This is the (optional) title',
11841  *             indicator: 'down',
11842  *             include: [ 'settings', 'stuff' ]
11843  *         }
11844  *     ] );
11846  *     // Create some UI around the toolbar and place it in the document
11847  *     var frame = new OO.ui.PanelLayout( {
11848  *         expanded: false,
11849  *         framed: true
11850  *     } );
11851  *     var contentFrame = new OO.ui.PanelLayout( {
11852  *         expanded: false,
11853  *         padded: true
11854  *     } );
11855  *     frame.$element.append(
11856  *         toolbar.$element,
11857  *         contentFrame.$element.append( $area )
11858  *     );
11859  *     $( 'body' ).append( frame.$element );
11861  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
11862  *     // document.
11863  *     toolbar.initialize();
11864  *     toolbar.emit( 'updateState' );
11866  * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
11867  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
11869  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11871  * @class
11872  * @extends OO.ui.PopupToolGroup
11874  * @constructor
11875  * @param {OO.ui.Toolbar} toolbar
11876  * @param {Object} [config] Configuration options
11877  */
11878 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
11879         // Allow passing positional parameters inside the config object
11880         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11881                 config = toolbar;
11882                 toolbar = config.toolbar;
11883         }
11885         // Configuration initialization
11886         config = config || {};
11888         // Parent constructor
11889         OO.ui.MenuToolGroup.parent.call( this, toolbar, config );
11891         // Events
11892         this.toolbar.connect( this, { updateState: 'onUpdateState' } );
11894         // Initialization
11895         this.$element.addClass( 'oo-ui-menuToolGroup' );
11898 /* Setup */
11900 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
11902 /* Static Properties */
11904 OO.ui.MenuToolGroup.static.name = 'menu';
11906 /* Methods */
11909  * Handle the toolbar state being updated.
11911  * When the state changes, the title of each active item in the menu will be joined together and
11912  * used as a label for the group. The label will be empty if none of the items are active.
11914  * @private
11915  */
11916 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
11917         var name,
11918                 labelTexts = [];
11920         for ( name in this.tools ) {
11921                 if ( this.tools[ name ].isActive() ) {
11922                         labelTexts.push( this.tools[ name ].getTitle() );
11923                 }
11924         }
11926         this.setLabel( labelTexts.join( ', ' ) || ' ' );
11930  * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
11931  * with a static name, title, and icon, as well with as any popup configurations. Unlike other tools, popup tools do not require that developers specify
11932  * an #onSelect or #onUpdateState method, as these methods have been implemented already.
11934  *     // Example of a popup tool. When selected, a popup tool displays
11935  *     // a popup window.
11936  *     function HelpTool( toolGroup, config ) {
11937  *        OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
11938  *            padded: true,
11939  *            label: 'Help',
11940  *            head: true
11941  *        } }, config ) );
11942  *        this.popup.$body.append( '<p>I am helpful!</p>' );
11943  *     };
11944  *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
11945  *     HelpTool.static.name = 'help';
11946  *     HelpTool.static.icon = 'help';
11947  *     HelpTool.static.title = 'Help';
11948  *     toolFactory.register( HelpTool );
11950  * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
11951  * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
11953  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11955  * @abstract
11956  * @class
11957  * @extends OO.ui.Tool
11958  * @mixins OO.ui.mixin.PopupElement
11960  * @constructor
11961  * @param {OO.ui.ToolGroup} toolGroup
11962  * @param {Object} [config] Configuration options
11963  */
11964 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
11965         // Allow passing positional parameters inside the config object
11966         if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
11967                 config = toolGroup;
11968                 toolGroup = config.toolGroup;
11969         }
11971         // Parent constructor
11972         OO.ui.PopupTool.parent.call( this, toolGroup, config );
11974         // Mixin constructors
11975         OO.ui.mixin.PopupElement.call( this, config );
11977         // Initialization
11978         this.$element
11979                 .addClass( 'oo-ui-popupTool' )
11980                 .append( this.popup.$element );
11983 /* Setup */
11985 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
11986 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
11988 /* Methods */
11991  * Handle the tool being selected.
11993  * @inheritdoc
11994  */
11995 OO.ui.PopupTool.prototype.onSelect = function () {
11996         if ( !this.isDisabled() ) {
11997                 this.popup.toggle();
11998         }
11999         this.setActive( false );
12000         return false;
12004  * Handle the toolbar state being updated.
12006  * @inheritdoc
12007  */
12008 OO.ui.PopupTool.prototype.onUpdateState = function () {
12009         this.setActive( false );
12013  * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
12014  * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
12015  * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
12016  * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
12017  * when the ToolGroupTool is selected.
12019  *     // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
12021  *     function SettingsTool() {
12022  *         SettingsTool.parent.apply( this, arguments );
12023  *     };
12024  *     OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
12025  *     SettingsTool.static.name = 'settings';
12026  *     SettingsTool.static.title = 'Change settings';
12027  *     SettingsTool.static.groupConfig = {
12028  *         icon: 'settings',
12029  *         label: 'ToolGroupTool',
12030  *         include: [  'setting1', 'setting2'  ]
12031  *     };
12032  *     toolFactory.register( SettingsTool );
12034  * For more information, please see the [OOjs UI documentation on MediaWiki][1].
12036  * Please note that this implementation is subject to change per [T74159] [2].
12038  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
12039  * [2]: https://phabricator.wikimedia.org/T74159
12041  * @abstract
12042  * @class
12043  * @extends OO.ui.Tool
12045  * @constructor
12046  * @param {OO.ui.ToolGroup} toolGroup
12047  * @param {Object} [config] Configuration options
12048  */
12049 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
12050         // Allow passing positional parameters inside the config object
12051         if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
12052                 config = toolGroup;
12053                 toolGroup = config.toolGroup;
12054         }
12056         // Parent constructor
12057         OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
12059         // Properties
12060         this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
12062         // Events
12063         this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
12065         // Initialization
12066         this.$link.remove();
12067         this.$element
12068                 .addClass( 'oo-ui-toolGroupTool' )
12069                 .append( this.innerToolGroup.$element );
12072 /* Setup */
12074 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
12076 /* Static Properties */
12079  * Toolgroup configuration.
12081  * The toolgroup configuration consists of the tools to include, as well as an icon and label
12082  * to use for the bar item. Tools can be included by symbolic name, group, or with the
12083  * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
12085  * @property {Object.<string,Array>}
12086  */
12087 OO.ui.ToolGroupTool.static.groupConfig = {};
12089 /* Methods */
12092  * Handle the tool being selected.
12094  * @inheritdoc
12095  */
12096 OO.ui.ToolGroupTool.prototype.onSelect = function () {
12097         this.innerToolGroup.setActive( !this.innerToolGroup.active );
12098         return false;
12102  * Synchronize disabledness state of the tool with the inner toolgroup.
12104  * @private
12105  * @param {boolean} disabled Element is disabled
12106  */
12107 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
12108         this.setDisabled( disabled );
12112  * Handle the toolbar state being updated.
12114  * @inheritdoc
12115  */
12116 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
12117         this.setActive( false );
12121  * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
12123  * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
12124  *  more information.
12125  * @return {OO.ui.ListToolGroup}
12126  */
12127 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
12128         if ( group.include === '*' ) {
12129                 // Apply defaults to catch-all groups
12130                 if ( group.label === undefined ) {
12131                         group.label = OO.ui.msg( 'ooui-toolbar-more' );
12132                 }
12133         }
12135         return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
12139  * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
12141  * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
12143  * @private
12144  * @abstract
12145  * @class
12146  * @extends OO.ui.mixin.GroupElement
12148  * @constructor
12149  * @param {Object} [config] Configuration options
12150  */
12151 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
12152         // Parent constructor
12153         OO.ui.mixin.GroupWidget.parent.call( this, config );
12156 /* Setup */
12158 OO.inheritClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
12160 /* Methods */
12163  * Set the disabled state of the widget.
12165  * This will also update the disabled state of child widgets.
12167  * @param {boolean} disabled Disable widget
12168  * @chainable
12169  */
12170 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
12171         var i, len;
12173         // Parent method
12174         // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
12175         OO.ui.Widget.prototype.setDisabled.call( this, disabled );
12177         // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
12178         if ( this.items ) {
12179                 for ( i = 0, len = this.items.length; i < len; i++ ) {
12180                         this.items[ i ].updateDisabled();
12181                 }
12182         }
12184         return this;
12188  * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
12190  * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
12191  * allows bidirectional communication.
12193  * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
12195  * @private
12196  * @abstract
12197  * @class
12199  * @constructor
12200  */
12201 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
12202         //
12205 /* Methods */
12208  * Check if widget is disabled.
12210  * Checks parent if present, making disabled state inheritable.
12212  * @return {boolean} Widget is disabled
12213  */
12214 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
12215         return this.disabled ||
12216                 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
12220  * Set group element is in.
12222  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
12223  * @chainable
12224  */
12225 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
12226         // Parent method
12227         // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
12228         OO.ui.Element.prototype.setElementGroup.call( this, group );
12230         // Initialize item disabled states
12231         this.updateDisabled();
12233         return this;
12237  * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
12238  * Controls include moving items up and down, removing items, and adding different kinds of items.
12240  * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
12242  * @class
12243  * @extends OO.ui.Widget
12244  * @mixins OO.ui.mixin.GroupElement
12245  * @mixins OO.ui.mixin.IconElement
12247  * @constructor
12248  * @param {OO.ui.OutlineSelectWidget} outline Outline to control
12249  * @param {Object} [config] Configuration options
12250  * @cfg {Object} [abilities] List of abilties
12251  * @cfg {boolean} [abilities.move=true] Allow moving movable items
12252  * @cfg {boolean} [abilities.remove=true] Allow removing removable items
12253  */
12254 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
12255         // Allow passing positional parameters inside the config object
12256         if ( OO.isPlainObject( outline ) && config === undefined ) {
12257                 config = outline;
12258                 outline = config.outline;
12259         }
12261         // Configuration initialization
12262         config = $.extend( { icon: 'add' }, config );
12264         // Parent constructor
12265         OO.ui.OutlineControlsWidget.parent.call( this, config );
12267         // Mixin constructors
12268         OO.ui.mixin.GroupElement.call( this, config );
12269         OO.ui.mixin.IconElement.call( this, config );
12271         // Properties
12272         this.outline = outline;
12273         this.$movers = $( '<div>' );
12274         this.upButton = new OO.ui.ButtonWidget( {
12275                 framed: false,
12276                 icon: 'collapse',
12277                 title: OO.ui.msg( 'ooui-outline-control-move-up' )
12278         } );
12279         this.downButton = new OO.ui.ButtonWidget( {
12280                 framed: false,
12281                 icon: 'expand',
12282                 title: OO.ui.msg( 'ooui-outline-control-move-down' )
12283         } );
12284         this.removeButton = new OO.ui.ButtonWidget( {
12285                 framed: false,
12286                 icon: 'remove',
12287                 title: OO.ui.msg( 'ooui-outline-control-remove' )
12288         } );
12289         this.abilities = { move: true, remove: true };
12291         // Events
12292         outline.connect( this, {
12293                 select: 'onOutlineChange',
12294                 add: 'onOutlineChange',
12295                 remove: 'onOutlineChange'
12296         } );
12297         this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
12298         this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
12299         this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
12301         // Initialization
12302         this.$element.addClass( 'oo-ui-outlineControlsWidget' );
12303         this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
12304         this.$movers
12305                 .addClass( 'oo-ui-outlineControlsWidget-movers' )
12306                 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
12307         this.$element.append( this.$icon, this.$group, this.$movers );
12308         this.setAbilities( config.abilities || {} );
12311 /* Setup */
12313 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
12314 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
12315 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.IconElement );
12317 /* Events */
12320  * @event move
12321  * @param {number} places Number of places to move
12322  */
12325  * @event remove
12326  */
12328 /* Methods */
12331  * Set abilities.
12333  * @param {Object} abilities List of abilties
12334  * @param {boolean} [abilities.move] Allow moving movable items
12335  * @param {boolean} [abilities.remove] Allow removing removable items
12336  */
12337 OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
12338         var ability;
12340         for ( ability in this.abilities ) {
12341                 if ( abilities[ ability ] !== undefined ) {
12342                         this.abilities[ ability ] = !!abilities[ ability ];
12343                 }
12344         }
12346         this.onOutlineChange();
12350  * @private
12351  * Handle outline change events.
12352  */
12353 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
12354         var i, len, firstMovable, lastMovable,
12355                 items = this.outline.getItems(),
12356                 selectedItem = this.outline.getSelectedItem(),
12357                 movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
12358                 removable = this.abilities.remove && selectedItem && selectedItem.isRemovable();
12360         if ( movable ) {
12361                 i = -1;
12362                 len = items.length;
12363                 while ( ++i < len ) {
12364                         if ( items[ i ].isMovable() ) {
12365                                 firstMovable = items[ i ];
12366                                 break;
12367                         }
12368                 }
12369                 i = len;
12370                 while ( i-- ) {
12371                         if ( items[ i ].isMovable() ) {
12372                                 lastMovable = items[ i ];
12373                                 break;
12374                         }
12375                 }
12376         }
12377         this.upButton.setDisabled( !movable || selectedItem === firstMovable );
12378         this.downButton.setDisabled( !movable || selectedItem === lastMovable );
12379         this.removeButton.setDisabled( !removable );
12383  * ToggleWidget implements basic behavior of widgets with an on/off state.
12384  * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
12386  * @abstract
12387  * @class
12388  * @extends OO.ui.Widget
12390  * @constructor
12391  * @param {Object} [config] Configuration options
12392  * @cfg {boolean} [value=false] The toggle’s initial on/off state.
12393  *  By default, the toggle is in the 'off' state.
12394  */
12395 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
12396         // Configuration initialization
12397         config = config || {};
12399         // Parent constructor
12400         OO.ui.ToggleWidget.parent.call( this, config );
12402         // Properties
12403         this.value = null;
12405         // Initialization
12406         this.$element.addClass( 'oo-ui-toggleWidget' );
12407         this.setValue( !!config.value );
12410 /* Setup */
12412 OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
12414 /* Events */
12417  * @event change
12419  * A change event is emitted when the on/off state of the toggle changes.
12421  * @param {boolean} value Value representing the new state of the toggle
12422  */
12424 /* Methods */
12427  * Get the value representing the toggle’s state.
12429  * @return {boolean} The on/off state of the toggle
12430  */
12431 OO.ui.ToggleWidget.prototype.getValue = function () {
12432         return this.value;
12436  * Set the state of the toggle: `true` for 'on', `false' for 'off'.
12438  * @param {boolean} value The state of the toggle
12439  * @fires change
12440  * @chainable
12441  */
12442 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
12443         value = !!value;
12444         if ( this.value !== value ) {
12445                 this.value = value;
12446                 this.emit( 'change', value );
12447                 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
12448                 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
12449                 this.$element.attr( 'aria-checked', value.toString() );
12450         }
12451         return this;
12455  * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
12456  * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
12457  * removed, and cleared from the group.
12459  *     @example
12460  *     // Example: A ButtonGroupWidget with two buttons
12461  *     var button1 = new OO.ui.PopupButtonWidget( {
12462  *         label: 'Select a category',
12463  *         icon: 'menu',
12464  *         popup: {
12465  *             $content: $( '<p>List of categories...</p>' ),
12466  *             padded: true,
12467  *             align: 'left'
12468  *         }
12469  *     } );
12470  *     var button2 = new OO.ui.ButtonWidget( {
12471  *         label: 'Add item'
12472  *     });
12473  *     var buttonGroup = new OO.ui.ButtonGroupWidget( {
12474  *         items: [button1, button2]
12475  *     } );
12476  *     $( 'body' ).append( buttonGroup.$element );
12478  * @class
12479  * @extends OO.ui.Widget
12480  * @mixins OO.ui.mixin.GroupElement
12482  * @constructor
12483  * @param {Object} [config] Configuration options
12484  * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
12485  */
12486 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
12487         // Configuration initialization
12488         config = config || {};
12490         // Parent constructor
12491         OO.ui.ButtonGroupWidget.parent.call( this, config );
12493         // Mixin constructors
12494         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
12496         // Initialization
12497         this.$element.addClass( 'oo-ui-buttonGroupWidget' );
12498         if ( Array.isArray( config.items ) ) {
12499                 this.addItems( config.items );
12500         }
12503 /* Setup */
12505 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
12506 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
12509  * ButtonWidget is a generic widget for buttons. A wide variety of looks,
12510  * feels, and functionality can be customized via the class’s configuration options
12511  * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
12512  * and examples.
12514  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
12516  *     @example
12517  *     // A button widget
12518  *     var button = new OO.ui.ButtonWidget( {
12519  *         label: 'Button with Icon',
12520  *         icon: 'remove',
12521  *         iconTitle: 'Remove'
12522  *     } );
12523  *     $( 'body' ).append( button.$element );
12525  * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
12527  * @class
12528  * @extends OO.ui.Widget
12529  * @mixins OO.ui.mixin.ButtonElement
12530  * @mixins OO.ui.mixin.IconElement
12531  * @mixins OO.ui.mixin.IndicatorElement
12532  * @mixins OO.ui.mixin.LabelElement
12533  * @mixins OO.ui.mixin.TitledElement
12534  * @mixins OO.ui.mixin.FlaggedElement
12535  * @mixins OO.ui.mixin.TabIndexedElement
12536  * @mixins OO.ui.mixin.AccessKeyedElement
12538  * @constructor
12539  * @param {Object} [config] Configuration options
12540  * @cfg {string} [href] Hyperlink to visit when the button is clicked.
12541  * @cfg {string} [target] The frame or window in which to open the hyperlink.
12542  * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
12543  */
12544 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
12545         // Configuration initialization
12546         config = config || {};
12548         // Parent constructor
12549         OO.ui.ButtonWidget.parent.call( this, config );
12551         // Mixin constructors
12552         OO.ui.mixin.ButtonElement.call( this, config );
12553         OO.ui.mixin.IconElement.call( this, config );
12554         OO.ui.mixin.IndicatorElement.call( this, config );
12555         OO.ui.mixin.LabelElement.call( this, config );
12556         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
12557         OO.ui.mixin.FlaggedElement.call( this, config );
12558         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
12559         OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$button } ) );
12561         // Properties
12562         this.href = null;
12563         this.target = null;
12564         this.noFollow = false;
12566         // Events
12567         this.connect( this, { disable: 'onDisable' } );
12569         // Initialization
12570         this.$button.append( this.$icon, this.$label, this.$indicator );
12571         this.$element
12572                 .addClass( 'oo-ui-buttonWidget' )
12573                 .append( this.$button );
12574         this.setHref( config.href );
12575         this.setTarget( config.target );
12576         this.setNoFollow( config.noFollow );
12579 /* Setup */
12581 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
12582 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
12583 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
12584 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
12585 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
12586 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
12587 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
12588 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
12589 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
12591 /* Methods */
12594  * @inheritdoc
12595  */
12596 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
12597         if ( !this.isDisabled() ) {
12598                 // Remove the tab-index while the button is down to prevent the button from stealing focus
12599                 this.$button.removeAttr( 'tabindex' );
12600         }
12602         return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
12606  * @inheritdoc
12607  */
12608 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
12609         if ( !this.isDisabled() ) {
12610                 // Restore the tab-index after the button is up to restore the button's accessibility
12611                 this.$button.attr( 'tabindex', this.tabIndex );
12612         }
12614         return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
12618  * Get hyperlink location.
12620  * @return {string} Hyperlink location
12621  */
12622 OO.ui.ButtonWidget.prototype.getHref = function () {
12623         return this.href;
12627  * Get hyperlink target.
12629  * @return {string} Hyperlink target
12630  */
12631 OO.ui.ButtonWidget.prototype.getTarget = function () {
12632         return this.target;
12636  * Get search engine traversal hint.
12638  * @return {boolean} Whether search engines should avoid traversing this hyperlink
12639  */
12640 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
12641         return this.noFollow;
12645  * Set hyperlink location.
12647  * @param {string|null} href Hyperlink location, null to remove
12648  */
12649 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
12650         href = typeof href === 'string' ? href : null;
12651         if ( href !== null ) {
12652                 if ( !OO.ui.isSafeUrl( href ) ) {
12653                         throw new Error( 'Potentially unsafe href provided: ' + href );
12654                 }
12656         }
12658         if ( href !== this.href ) {
12659                 this.href = href;
12660                 this.updateHref();
12661         }
12663         return this;
12667  * Update the `href` attribute, in case of changes to href or
12668  * disabled state.
12670  * @private
12671  * @chainable
12672  */
12673 OO.ui.ButtonWidget.prototype.updateHref = function () {
12674         if ( this.href !== null && !this.isDisabled() ) {
12675                 this.$button.attr( 'href', this.href );
12676         } else {
12677                 this.$button.removeAttr( 'href' );
12678         }
12680         return this;
12684  * Handle disable events.
12686  * @private
12687  * @param {boolean} disabled Element is disabled
12688  */
12689 OO.ui.ButtonWidget.prototype.onDisable = function () {
12690         this.updateHref();
12694  * Set hyperlink target.
12696  * @param {string|null} target Hyperlink target, null to remove
12697  */
12698 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
12699         target = typeof target === 'string' ? target : null;
12701         if ( target !== this.target ) {
12702                 this.target = target;
12703                 if ( target !== null ) {
12704                         this.$button.attr( 'target', target );
12705                 } else {
12706                         this.$button.removeAttr( 'target' );
12707                 }
12708         }
12710         return this;
12714  * Set search engine traversal hint.
12716  * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
12717  */
12718 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
12719         noFollow = typeof noFollow === 'boolean' ? noFollow : true;
12721         if ( noFollow !== this.noFollow ) {
12722                 this.noFollow = noFollow;
12723                 if ( noFollow ) {
12724                         this.$button.attr( 'rel', 'nofollow' );
12725                 } else {
12726                         this.$button.removeAttr( 'rel' );
12727                 }
12728         }
12730         return this;
12734  * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
12735  * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
12736  * of the actions.
12738  * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
12739  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
12740  * and examples.
12742  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
12744  * @class
12745  * @extends OO.ui.ButtonWidget
12746  * @mixins OO.ui.mixin.PendingElement
12748  * @constructor
12749  * @param {Object} [config] Configuration options
12750  * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12751  * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
12752  *  should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
12753  *  for more information about setting modes.
12754  * @cfg {boolean} [framed=false] Render the action button with a frame
12755  */
12756 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
12757         // Configuration initialization
12758         config = $.extend( { framed: false }, config );
12760         // Parent constructor
12761         OO.ui.ActionWidget.parent.call( this, config );
12763         // Mixin constructors
12764         OO.ui.mixin.PendingElement.call( this, config );
12766         // Properties
12767         this.action = config.action || '';
12768         this.modes = config.modes || [];
12769         this.width = 0;
12770         this.height = 0;
12772         // Initialization
12773         this.$element.addClass( 'oo-ui-actionWidget' );
12776 /* Setup */
12778 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
12779 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
12781 /* Events */
12784  * A resize event is emitted when the size of the widget changes.
12786  * @event resize
12787  */
12789 /* Methods */
12792  * Check if the action is configured to be available in the specified `mode`.
12794  * @param {string} mode Name of mode
12795  * @return {boolean} The action is configured with the mode
12796  */
12797 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
12798         return this.modes.indexOf( mode ) !== -1;
12802  * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12804  * @return {string}
12805  */
12806 OO.ui.ActionWidget.prototype.getAction = function () {
12807         return this.action;
12811  * Get the symbolic name of the mode or modes for which the action is configured to be available.
12813  * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
12814  * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
12815  * are hidden.
12817  * @return {string[]}
12818  */
12819 OO.ui.ActionWidget.prototype.getModes = function () {
12820         return this.modes.slice();
12824  * Emit a resize event if the size has changed.
12826  * @private
12827  * @chainable
12828  */
12829 OO.ui.ActionWidget.prototype.propagateResize = function () {
12830         var width, height;
12832         if ( this.isElementAttached() ) {
12833                 width = this.$element.width();
12834                 height = this.$element.height();
12836                 if ( width !== this.width || height !== this.height ) {
12837                         this.width = width;
12838                         this.height = height;
12839                         this.emit( 'resize' );
12840                 }
12841         }
12843         return this;
12847  * @inheritdoc
12848  */
12849 OO.ui.ActionWidget.prototype.setIcon = function () {
12850         // Mixin method
12851         OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
12852         this.propagateResize();
12854         return this;
12858  * @inheritdoc
12859  */
12860 OO.ui.ActionWidget.prototype.setLabel = function () {
12861         // Mixin method
12862         OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
12863         this.propagateResize();
12865         return this;
12869  * @inheritdoc
12870  */
12871 OO.ui.ActionWidget.prototype.setFlags = function () {
12872         // Mixin method
12873         OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
12874         this.propagateResize();
12876         return this;
12880  * @inheritdoc
12881  */
12882 OO.ui.ActionWidget.prototype.clearFlags = function () {
12883         // Mixin method
12884         OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
12885         this.propagateResize();
12887         return this;
12891  * Toggle the visibility of the action button.
12893  * @param {boolean} [show] Show button, omit to toggle visibility
12894  * @chainable
12895  */
12896 OO.ui.ActionWidget.prototype.toggle = function () {
12897         // Parent method
12898         OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
12899         this.propagateResize();
12901         return this;
12905  * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
12906  * which is used to display additional information or options.
12908  *     @example
12909  *     // Example of a popup button.
12910  *     var popupButton = new OO.ui.PopupButtonWidget( {
12911  *         label: 'Popup button with options',
12912  *         icon: 'menu',
12913  *         popup: {
12914  *             $content: $( '<p>Additional options here.</p>' ),
12915  *             padded: true,
12916  *             align: 'force-left'
12917  *         }
12918  *     } );
12919  *     // Append the button to the DOM.
12920  *     $( 'body' ).append( popupButton.$element );
12922  * @class
12923  * @extends OO.ui.ButtonWidget
12924  * @mixins OO.ui.mixin.PopupElement
12926  * @constructor
12927  * @param {Object} [config] Configuration options
12928  */
12929 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
12930         // Parent constructor
12931         OO.ui.PopupButtonWidget.parent.call( this, config );
12933         // Mixin constructors
12934         OO.ui.mixin.PopupElement.call( this, config );
12936         // Events
12937         this.connect( this, { click: 'onAction' } );
12939         // Initialization
12940         this.$element
12941                 .addClass( 'oo-ui-popupButtonWidget' )
12942                 .attr( 'aria-haspopup', 'true' )
12943                 .append( this.popup.$element );
12946 /* Setup */
12948 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
12949 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
12951 /* Methods */
12954  * Handle the button action being triggered.
12956  * @private
12957  */
12958 OO.ui.PopupButtonWidget.prototype.onAction = function () {
12959         this.popup.toggle();
12963  * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
12964  * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
12965  * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
12966  * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
12967  * and {@link OO.ui.mixin.LabelElement labels}. Please see
12968  * the [OOjs UI documentation][1] on MediaWiki for more information.
12970  *     @example
12971  *     // Toggle buttons in the 'off' and 'on' state.
12972  *     var toggleButton1 = new OO.ui.ToggleButtonWidget( {
12973  *         label: 'Toggle Button off'
12974  *     } );
12975  *     var toggleButton2 = new OO.ui.ToggleButtonWidget( {
12976  *         label: 'Toggle Button on',
12977  *         value: true
12978  *     } );
12979  *     // Append the buttons to the DOM.
12980  *     $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
12982  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
12984  * @class
12985  * @extends OO.ui.ToggleWidget
12986  * @mixins OO.ui.mixin.ButtonElement
12987  * @mixins OO.ui.mixin.IconElement
12988  * @mixins OO.ui.mixin.IndicatorElement
12989  * @mixins OO.ui.mixin.LabelElement
12990  * @mixins OO.ui.mixin.TitledElement
12991  * @mixins OO.ui.mixin.FlaggedElement
12992  * @mixins OO.ui.mixin.TabIndexedElement
12994  * @constructor
12995  * @param {Object} [config] Configuration options
12996  * @cfg {boolean} [value=false] The toggle button’s initial on/off
12997  *  state. By default, the button is in the 'off' state.
12998  */
12999 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
13000         // Configuration initialization
13001         config = config || {};
13003         // Parent constructor
13004         OO.ui.ToggleButtonWidget.parent.call( this, config );
13006         // Mixin constructors
13007         OO.ui.mixin.ButtonElement.call( this, config );
13008         OO.ui.mixin.IconElement.call( this, config );
13009         OO.ui.mixin.IndicatorElement.call( this, config );
13010         OO.ui.mixin.LabelElement.call( this, config );
13011         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
13012         OO.ui.mixin.FlaggedElement.call( this, config );
13013         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
13015         // Events
13016         this.connect( this, { click: 'onAction' } );
13018         // Initialization
13019         this.$button.append( this.$icon, this.$label, this.$indicator );
13020         this.$element
13021                 .addClass( 'oo-ui-toggleButtonWidget' )
13022                 .append( this.$button );
13025 /* Setup */
13027 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
13028 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
13029 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
13030 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
13031 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
13032 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TitledElement );
13033 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
13034 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
13036 /* Methods */
13039  * Handle the button action being triggered.
13041  * @private
13042  */
13043 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
13044         this.setValue( !this.value );
13048  * @inheritdoc
13049  */
13050 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
13051         value = !!value;
13052         if ( value !== this.value ) {
13053                 // Might be called from parent constructor before ButtonElement constructor
13054                 if ( this.$button ) {
13055                         this.$button.attr( 'aria-pressed', value.toString() );
13056                 }
13057                 this.setActive( value );
13058         }
13060         // Parent method
13061         OO.ui.ToggleButtonWidget.parent.prototype.setValue.call( this, value );
13063         return this;
13067  * @inheritdoc
13068  */
13069 OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
13070         if ( this.$button ) {
13071                 this.$button.removeAttr( 'aria-pressed' );
13072         }
13073         OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
13074         this.$button.attr( 'aria-pressed', this.value.toString() );
13078  * CapsuleMultiSelectWidgets are something like a {@link OO.ui.ComboBoxWidget combo box widget}
13079  * that allows for selecting multiple values.
13081  * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
13083  *     @example
13084  *     // Example: A CapsuleMultiSelectWidget.
13085  *     var capsule = new OO.ui.CapsuleMultiSelectWidget( {
13086  *         label: 'CapsuleMultiSelectWidget',
13087  *         selected: [ 'Option 1', 'Option 3' ],
13088  *         menu: {
13089  *             items: [
13090  *                 new OO.ui.MenuOptionWidget( {
13091  *                     data: 'Option 1',
13092  *                     label: 'Option One'
13093  *                 } ),
13094  *                 new OO.ui.MenuOptionWidget( {
13095  *                     data: 'Option 2',
13096  *                     label: 'Option Two'
13097  *                 } ),
13098  *                 new OO.ui.MenuOptionWidget( {
13099  *                     data: 'Option 3',
13100  *                     label: 'Option Three'
13101  *                 } ),
13102  *                 new OO.ui.MenuOptionWidget( {
13103  *                     data: 'Option 4',
13104  *                     label: 'Option Four'
13105  *                 } ),
13106  *                 new OO.ui.MenuOptionWidget( {
13107  *                     data: 'Option 5',
13108  *                     label: 'Option Five'
13109  *                 } )
13110  *             ]
13111  *         }
13112  *     } );
13113  *     $( 'body' ).append( capsule.$element );
13115  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
13117  * @class
13118  * @extends OO.ui.Widget
13119  * @mixins OO.ui.mixin.TabIndexedElement
13120  * @mixins OO.ui.mixin.GroupElement
13122  * @constructor
13123  * @param {Object} [config] Configuration options
13124  * @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if not present in the menu.
13125  * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
13126  * @cfg {Object} [popup] Configuration options to pass to the {@link OO.ui.PopupWidget popup widget}.
13127  *  If specified, this popup will be shown instead of the menu (but the menu
13128  *  will still be used for item labels and allowArbitrary=false). The widgets
13129  *  in the popup should use this.addItemsFromData() or this.addItems() as necessary.
13130  * @cfg {jQuery} [$overlay] Render the menu or popup into a separate layer.
13131  *  This configuration is useful in cases where the expanded menu is larger than
13132  *  its containing `<div>`. The specified overlay layer is usually on top of
13133  *  the containing `<div>` and has a larger area. By default, the menu uses
13134  *  relative positioning.
13135  */
13136 OO.ui.CapsuleMultiSelectWidget = function OoUiCapsuleMultiSelectWidget( config ) {
13137         var $tabFocus;
13139         // Configuration initialization
13140         config = config || {};
13142         // Parent constructor
13143         OO.ui.CapsuleMultiSelectWidget.parent.call( this, config );
13145         // Properties (must be set before mixin constructor calls)
13146         this.$input = config.popup ? null : $( '<input>' );
13147         this.$handle = $( '<div>' );
13149         // Mixin constructors
13150         OO.ui.mixin.GroupElement.call( this, config );
13151         if ( config.popup ) {
13152                 config.popup = $.extend( {}, config.popup, {
13153                         align: 'forwards',
13154                         anchor: false
13155                 } );
13156                 OO.ui.mixin.PopupElement.call( this, config );
13157                 $tabFocus = $( '<span>' );
13158                 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: $tabFocus } ) );
13159         } else {
13160                 this.popup = null;
13161                 $tabFocus = null;
13162                 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
13163         }
13164         OO.ui.mixin.IndicatorElement.call( this, config );
13165         OO.ui.mixin.IconElement.call( this, config );
13167         // Properties
13168         this.allowArbitrary = !!config.allowArbitrary;
13169         this.$overlay = config.$overlay || this.$element;
13170         this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
13171                 {
13172                         widget: this,
13173                         $input: this.$input,
13174                         $container: this.$element,
13175                         filterFromInput: true,
13176                         disabled: this.isDisabled()
13177                 },
13178                 config.menu
13179         ) );
13181         // Events
13182         if ( this.popup ) {
13183                 $tabFocus.on( {
13184                         focus: this.onFocusForPopup.bind( this )
13185                 } );
13186                 this.popup.$element.on( 'focusout', this.onPopupFocusOut.bind( this ) );
13187                 if ( this.popup.$autoCloseIgnore ) {
13188                         this.popup.$autoCloseIgnore.on( 'focusout', this.onPopupFocusOut.bind( this ) );
13189                 }
13190                 this.popup.connect( this, {
13191                         toggle: function ( visible ) {
13192                                 $tabFocus.toggle( !visible );
13193                         }
13194                 } );
13195         } else {
13196                 this.$input.on( {
13197                         focus: this.onInputFocus.bind( this ),
13198                         blur: this.onInputBlur.bind( this ),
13199                         'propertychange change click mouseup keydown keyup input cut paste select': this.onInputChange.bind( this ),
13200                         keydown: this.onKeyDown.bind( this ),
13201                         keypress: this.onKeyPress.bind( this )
13202                 } );
13203         }
13204         this.menu.connect( this, {
13205                 choose: 'onMenuChoose',
13206                 add: 'onMenuItemsChange',
13207                 remove: 'onMenuItemsChange'
13208         } );
13209         this.$handle.on( {
13210                 click: this.onClick.bind( this )
13211         } );
13213         // Initialization
13214         if ( this.$input ) {
13215                 this.$input.prop( 'disabled', this.isDisabled() );
13216                 this.$input.attr( {
13217                         role: 'combobox',
13218                         'aria-autocomplete': 'list'
13219                 } );
13220                 this.$input.width( '1em' );
13221         }
13222         if ( config.data ) {
13223                 this.setItemsFromData( config.data );
13224         }
13225         this.$group.addClass( 'oo-ui-capsuleMultiSelectWidget-group' );
13226         this.$handle.addClass( 'oo-ui-capsuleMultiSelectWidget-handle' )
13227                 .append( this.$indicator, this.$icon, this.$group );
13228         this.$element.addClass( 'oo-ui-capsuleMultiSelectWidget' )
13229                 .append( this.$handle );
13230         if ( this.popup ) {
13231                 this.$handle.append( $tabFocus );
13232                 this.$overlay.append( this.popup.$element );
13233         } else {
13234                 this.$handle.append( this.$input );
13235                 this.$overlay.append( this.menu.$element );
13236         }
13237         this.onMenuItemsChange();
13240 /* Setup */
13242 OO.inheritClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.Widget );
13243 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.GroupElement );
13244 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.PopupElement );
13245 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.TabIndexedElement );
13246 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IndicatorElement );
13247 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IconElement );
13249 /* Events */
13252  * @event change
13254  * A change event is emitted when the set of selected items changes.
13256  * @param {Mixed[]} datas Data of the now-selected items
13257  */
13259 /* Methods */
13262  * Construct a OO.ui.CapsuleItemWidget (or a subclass thereof) from given label and data.
13264  * @protected
13265  * @param {Mixed} data Custom data of any type.
13266  * @param {string} label The label text.
13267  * @return {OO.ui.CapsuleItemWidget}
13268  */
13269 OO.ui.CapsuleMultiSelectWidget.prototype.createItemWidget = function ( data, label ) {
13270         return new OO.ui.CapsuleItemWidget( { data: data, label: label } );
13274  * Get the data of the items in the capsule
13275  * @return {Mixed[]}
13276  */
13277 OO.ui.CapsuleMultiSelectWidget.prototype.getItemsData = function () {
13278         return $.map( this.getItems(), function ( e ) { return e.data; } );
13282  * Set the items in the capsule by providing data
13283  * @chainable
13284  * @param {Mixed[]} datas
13285  * @return {OO.ui.CapsuleMultiSelectWidget}
13286  */
13287 OO.ui.CapsuleMultiSelectWidget.prototype.setItemsFromData = function ( datas ) {
13288         var widget = this,
13289                 menu = this.menu,
13290                 items = this.getItems();
13292         $.each( datas, function ( i, data ) {
13293                 var j, label,
13294                         item = menu.getItemFromData( data );
13296                 if ( item ) {
13297                         label = item.label;
13298                 } else if ( widget.allowArbitrary ) {
13299                         label = String( data );
13300                 } else {
13301                         return;
13302                 }
13304                 item = null;
13305                 for ( j = 0; j < items.length; j++ ) {
13306                         if ( items[ j ].data === data && items[ j ].label === label ) {
13307                                 item = items[ j ];
13308                                 items.splice( j, 1 );
13309                                 break;
13310                         }
13311                 }
13312                 if ( !item ) {
13313                         item = widget.createItemWidget( data, label );
13314                 }
13315                 widget.addItems( [ item ], i );
13316         } );
13318         if ( items.length ) {
13319                 widget.removeItems( items );
13320         }
13322         return this;
13326  * Add items to the capsule by providing their data
13327  * @chainable
13328  * @param {Mixed[]} datas
13329  * @return {OO.ui.CapsuleMultiSelectWidget}
13330  */
13331 OO.ui.CapsuleMultiSelectWidget.prototype.addItemsFromData = function ( datas ) {
13332         var widget = this,
13333                 menu = this.menu,
13334                 items = [];
13336         $.each( datas, function ( i, data ) {
13337                 var item;
13339                 if ( !widget.getItemFromData( data ) ) {
13340                         item = menu.getItemFromData( data );
13341                         if ( item ) {
13342                                 items.push( widget.createItemWidget( data, item.label ) );
13343                         } else if ( widget.allowArbitrary ) {
13344                                 items.push( widget.createItemWidget( data, String( data ) ) );
13345                         }
13346                 }
13347         } );
13349         if ( items.length ) {
13350                 this.addItems( items );
13351         }
13353         return this;
13357  * Remove items by data
13358  * @chainable
13359  * @param {Mixed[]} datas
13360  * @return {OO.ui.CapsuleMultiSelectWidget}
13361  */
13362 OO.ui.CapsuleMultiSelectWidget.prototype.removeItemsFromData = function ( datas ) {
13363         var widget = this,
13364                 items = [];
13366         $.each( datas, function ( i, data ) {
13367                 var item = widget.getItemFromData( data );
13368                 if ( item ) {
13369                         items.push( item );
13370                 }
13371         } );
13373         if ( items.length ) {
13374                 this.removeItems( items );
13375         }
13377         return this;
13381  * @inheritdoc
13382  */
13383 OO.ui.CapsuleMultiSelectWidget.prototype.addItems = function ( items ) {
13384         var same, i, l,
13385                 oldItems = this.items.slice();
13387         OO.ui.mixin.GroupElement.prototype.addItems.call( this, items );
13389         if ( this.items.length !== oldItems.length ) {
13390                 same = false;
13391         } else {
13392                 same = true;
13393                 for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
13394                         same = same && this.items[ i ] === oldItems[ i ];
13395                 }
13396         }
13397         if ( !same ) {
13398                 this.emit( 'change', this.getItemsData() );
13399         }
13401         return this;
13405  * @inheritdoc
13406  */
13407 OO.ui.CapsuleMultiSelectWidget.prototype.removeItems = function ( items ) {
13408         var same, i, l,
13409                 oldItems = this.items.slice();
13411         OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
13413         if ( this.items.length !== oldItems.length ) {
13414                 same = false;
13415         } else {
13416                 same = true;
13417                 for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
13418                         same = same && this.items[ i ] === oldItems[ i ];
13419                 }
13420         }
13421         if ( !same ) {
13422                 this.emit( 'change', this.getItemsData() );
13423         }
13425         return this;
13429  * @inheritdoc
13430  */
13431 OO.ui.CapsuleMultiSelectWidget.prototype.clearItems = function () {
13432         if ( this.items.length ) {
13433                 OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
13434                 this.emit( 'change', this.getItemsData() );
13435         }
13436         return this;
13440  * Get the capsule widget's menu.
13441  * @return {OO.ui.MenuSelectWidget} Menu widget
13442  */
13443 OO.ui.CapsuleMultiSelectWidget.prototype.getMenu = function () {
13444         return this.menu;
13448  * Handle focus events
13450  * @private
13451  * @param {jQuery.Event} event
13452  */
13453 OO.ui.CapsuleMultiSelectWidget.prototype.onInputFocus = function () {
13454         if ( !this.isDisabled() ) {
13455                 this.menu.toggle( true );
13456         }
13460  * Handle blur events
13462  * @private
13463  * @param {jQuery.Event} event
13464  */
13465 OO.ui.CapsuleMultiSelectWidget.prototype.onInputBlur = function () {
13466         if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
13467                 this.addItemsFromData( [ this.$input.val() ] );
13468         }
13469         this.clearInput();
13473  * Handle focus events
13475  * @private
13476  * @param {jQuery.Event} event
13477  */
13478 OO.ui.CapsuleMultiSelectWidget.prototype.onFocusForPopup = function () {
13479         if ( !this.isDisabled() ) {
13480                 this.popup.setSize( this.$handle.width() );
13481                 this.popup.toggle( true );
13482                 this.popup.$element.find( '*' )
13483                         .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
13484                         .first()
13485                         .focus();
13486         }
13490  * Handles popup focus out events.
13492  * @private
13493  * @param {Event} e Focus out event
13494  */
13495 OO.ui.CapsuleMultiSelectWidget.prototype.onPopupFocusOut = function () {
13496         var widget = this.popup;
13498         setTimeout( function () {
13499                 if (
13500                         widget.isVisible() &&
13501                         !OO.ui.contains( widget.$element[ 0 ], document.activeElement, true ) &&
13502                         ( !widget.$autoCloseIgnore || !widget.$autoCloseIgnore.has( document.activeElement ).length )
13503                 ) {
13504                         widget.toggle( false );
13505                 }
13506         } );
13510  * Handle mouse click events.
13512  * @private
13513  * @param {jQuery.Event} e Mouse click event
13514  */
13515 OO.ui.CapsuleMultiSelectWidget.prototype.onClick = function ( e ) {
13516         if ( e.which === 1 ) {
13517                 this.focus();
13518                 return false;
13519         }
13523  * Handle key press events.
13525  * @private
13526  * @param {jQuery.Event} e Key press event
13527  */
13528 OO.ui.CapsuleMultiSelectWidget.prototype.onKeyPress = function ( e ) {
13529         var item;
13531         if ( !this.isDisabled() ) {
13532                 if ( e.which === OO.ui.Keys.ESCAPE ) {
13533                         this.clearInput();
13534                         return false;
13535                 }
13537                 if ( !this.popup ) {
13538                         this.menu.toggle( true );
13539                         if ( e.which === OO.ui.Keys.ENTER ) {
13540                                 item = this.menu.getItemFromLabel( this.$input.val(), true );
13541                                 if ( item ) {
13542                                         this.addItemsFromData( [ item.data ] );
13543                                         this.clearInput();
13544                                 } else if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
13545                                         this.addItemsFromData( [ this.$input.val() ] );
13546                                         this.clearInput();
13547                                 }
13548                                 return false;
13549                         }
13551                         // Make sure the input gets resized.
13552                         setTimeout( this.onInputChange.bind( this ), 0 );
13553                 }
13554         }
13558  * Handle key down events.
13560  * @private
13561  * @param {jQuery.Event} e Key down event
13562  */
13563 OO.ui.CapsuleMultiSelectWidget.prototype.onKeyDown = function ( e ) {
13564         if ( !this.isDisabled() ) {
13565                 // 'keypress' event is not triggered for Backspace
13566                 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.$input.val() === '' ) {
13567                         if ( this.items.length ) {
13568                                 this.removeItems( this.items.slice( -1 ) );
13569                         }
13570                         return false;
13571                 }
13572         }
13576  * Handle input change events.
13578  * @private
13579  * @param {jQuery.Event} e Event of some sort
13580  */
13581 OO.ui.CapsuleMultiSelectWidget.prototype.onInputChange = function () {
13582         if ( !this.isDisabled() ) {
13583                 this.$input.width( this.$input.val().length + 'em' );
13584         }
13588  * Handle menu choose events.
13590  * @private
13591  * @param {OO.ui.OptionWidget} item Chosen item
13592  */
13593 OO.ui.CapsuleMultiSelectWidget.prototype.onMenuChoose = function ( item ) {
13594         if ( item && item.isVisible() ) {
13595                 this.addItemsFromData( [ item.getData() ] );
13596                 this.clearInput();
13597         }
13601  * Handle menu item change events.
13603  * @private
13604  */
13605 OO.ui.CapsuleMultiSelectWidget.prototype.onMenuItemsChange = function () {
13606         this.setItemsFromData( this.getItemsData() );
13607         this.$element.toggleClass( 'oo-ui-capsuleMultiSelectWidget-empty', this.menu.isEmpty() );
13611  * Clear the input field
13612  * @private
13613  */
13614 OO.ui.CapsuleMultiSelectWidget.prototype.clearInput = function () {
13615         if ( this.$input ) {
13616                 this.$input.val( '' );
13617                 this.$input.width( '1em' );
13618         }
13619         if ( this.popup ) {
13620                 this.popup.toggle( false );
13621         }
13622         this.menu.toggle( false );
13623         this.menu.selectItem();
13624         this.menu.highlightItem();
13628  * @inheritdoc
13629  */
13630 OO.ui.CapsuleMultiSelectWidget.prototype.setDisabled = function ( disabled ) {
13631         var i, len;
13633         // Parent method
13634         OO.ui.CapsuleMultiSelectWidget.parent.prototype.setDisabled.call( this, disabled );
13636         if ( this.$input ) {
13637                 this.$input.prop( 'disabled', this.isDisabled() );
13638         }
13639         if ( this.menu ) {
13640                 this.menu.setDisabled( this.isDisabled() );
13641         }
13642         if ( this.popup ) {
13643                 this.popup.setDisabled( this.isDisabled() );
13644         }
13646         if ( this.items ) {
13647                 for ( i = 0, len = this.items.length; i < len; i++ ) {
13648                         this.items[ i ].updateDisabled();
13649                 }
13650         }
13652         return this;
13656  * Focus the widget
13657  * @chainable
13658  * @return {OO.ui.CapsuleMultiSelectWidget}
13659  */
13660 OO.ui.CapsuleMultiSelectWidget.prototype.focus = function () {
13661         if ( !this.isDisabled() ) {
13662                 if ( this.popup ) {
13663                         this.popup.setSize( this.$handle.width() );
13664                         this.popup.toggle( true );
13665                         this.popup.$element.find( '*' )
13666                                 .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
13667                                 .first()
13668                                 .focus();
13669                 } else {
13670                         this.menu.toggle( true );
13671                         this.$input.focus();
13672                 }
13673         }
13674         return this;
13678  * CapsuleItemWidgets are used within a {@link OO.ui.CapsuleMultiSelectWidget
13679  * CapsuleMultiSelectWidget} to display the selected items.
13681  * @class
13682  * @extends OO.ui.Widget
13683  * @mixins OO.ui.mixin.ItemWidget
13684  * @mixins OO.ui.mixin.IndicatorElement
13685  * @mixins OO.ui.mixin.LabelElement
13686  * @mixins OO.ui.mixin.FlaggedElement
13687  * @mixins OO.ui.mixin.TabIndexedElement
13689  * @constructor
13690  * @param {Object} [config] Configuration options
13691  */
13692 OO.ui.CapsuleItemWidget = function OoUiCapsuleItemWidget( config ) {
13693         // Configuration initialization
13694         config = config || {};
13696         // Parent constructor
13697         OO.ui.CapsuleItemWidget.parent.call( this, config );
13699         // Properties (must be set before mixin constructor calls)
13700         this.$indicator = $( '<span>' );
13702         // Mixin constructors
13703         OO.ui.mixin.ItemWidget.call( this );
13704         OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$indicator, indicator: 'clear' } ) );
13705         OO.ui.mixin.LabelElement.call( this, config );
13706         OO.ui.mixin.FlaggedElement.call( this, config );
13707         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
13709         // Events
13710         this.$indicator.on( {
13711                 keydown: this.onCloseKeyDown.bind( this ),
13712                 click: this.onCloseClick.bind( this )
13713         } );
13715         // Initialization
13716         this.$element
13717                 .addClass( 'oo-ui-capsuleItemWidget' )
13718                 .append( this.$indicator, this.$label );
13721 /* Setup */
13723 OO.inheritClass( OO.ui.CapsuleItemWidget, OO.ui.Widget );
13724 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.ItemWidget );
13725 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.IndicatorElement );
13726 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.LabelElement );
13727 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.FlaggedElement );
13728 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.TabIndexedElement );
13730 /* Methods */
13733  * Handle close icon clicks
13734  * @param {jQuery.Event} event
13735  */
13736 OO.ui.CapsuleItemWidget.prototype.onCloseClick = function () {
13737         var element = this.getElementGroup();
13739         if ( !this.isDisabled() && element && $.isFunction( element.removeItems ) ) {
13740                 element.removeItems( [ this ] );
13741                 element.focus();
13742         }
13746  * Handle close keyboard events
13747  * @param {jQuery.Event} event Key down event
13748  */
13749 OO.ui.CapsuleItemWidget.prototype.onCloseKeyDown = function ( e ) {
13750         if ( !this.isDisabled() && $.isFunction( this.getElementGroup().removeItems ) ) {
13751                 switch ( e.which ) {
13752                         case OO.ui.Keys.ENTER:
13753                         case OO.ui.Keys.BACKSPACE:
13754                         case OO.ui.Keys.SPACE:
13755                                 this.getElementGroup().removeItems( [ this ] );
13756                                 return false;
13757                 }
13758         }
13762  * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
13763  * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
13764  * users can interact with it.
13766  * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
13767  * OO.ui.DropdownInputWidget instead.
13769  *     @example
13770  *     // Example: A DropdownWidget with a menu that contains three options
13771  *     var dropDown = new OO.ui.DropdownWidget( {
13772  *         label: 'Dropdown menu: Select a menu option',
13773  *         menu: {
13774  *             items: [
13775  *                 new OO.ui.MenuOptionWidget( {
13776  *                     data: 'a',
13777  *                     label: 'First'
13778  *                 } ),
13779  *                 new OO.ui.MenuOptionWidget( {
13780  *                     data: 'b',
13781  *                     label: 'Second'
13782  *                 } ),
13783  *                 new OO.ui.MenuOptionWidget( {
13784  *                     data: 'c',
13785  *                     label: 'Third'
13786  *                 } )
13787  *             ]
13788  *         }
13789  *     } );
13791  *     $( 'body' ).append( dropDown.$element );
13793  *     dropDown.getMenu().selectItemByData( 'b' );
13795  *     dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
13797  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
13799  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
13801  * @class
13802  * @extends OO.ui.Widget
13803  * @mixins OO.ui.mixin.IconElement
13804  * @mixins OO.ui.mixin.IndicatorElement
13805  * @mixins OO.ui.mixin.LabelElement
13806  * @mixins OO.ui.mixin.TitledElement
13807  * @mixins OO.ui.mixin.TabIndexedElement
13809  * @constructor
13810  * @param {Object} [config] Configuration options
13811  * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
13812  * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
13813  *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
13814  *  containing `<div>` and has a larger area. By default, the menu uses relative positioning.
13815  */
13816 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
13817         // Configuration initialization
13818         config = $.extend( { indicator: 'down' }, config );
13820         // Parent constructor
13821         OO.ui.DropdownWidget.parent.call( this, config );
13823         // Properties (must be set before TabIndexedElement constructor call)
13824         this.$handle = this.$( '<span>' );
13825         this.$overlay = config.$overlay || this.$element;
13827         // Mixin constructors
13828         OO.ui.mixin.IconElement.call( this, config );
13829         OO.ui.mixin.IndicatorElement.call( this, config );
13830         OO.ui.mixin.LabelElement.call( this, config );
13831         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
13832         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
13834         // Properties
13835         this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend( {
13836                 widget: this,
13837                 $container: this.$element
13838         }, config.menu ) );
13840         // Events
13841         this.$handle.on( {
13842                 click: this.onClick.bind( this ),
13843                 keypress: this.onKeyPress.bind( this )
13844         } );
13845         this.menu.connect( this, { select: 'onMenuSelect' } );
13847         // Initialization
13848         this.$handle
13849                 .addClass( 'oo-ui-dropdownWidget-handle' )
13850                 .append( this.$icon, this.$label, this.$indicator );
13851         this.$element
13852                 .addClass( 'oo-ui-dropdownWidget' )
13853                 .append( this.$handle );
13854         this.$overlay.append( this.menu.$element );
13857 /* Setup */
13859 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
13860 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
13861 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
13862 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
13863 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
13864 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
13866 /* Methods */
13869  * Get the menu.
13871  * @return {OO.ui.MenuSelectWidget} Menu of widget
13872  */
13873 OO.ui.DropdownWidget.prototype.getMenu = function () {
13874         return this.menu;
13878  * Handles menu select events.
13880  * @private
13881  * @param {OO.ui.MenuOptionWidget} item Selected menu item
13882  */
13883 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
13884         var selectedLabel;
13886         if ( !item ) {
13887                 this.setLabel( null );
13888                 return;
13889         }
13891         selectedLabel = item.getLabel();
13893         // If the label is a DOM element, clone it, because setLabel will append() it
13894         if ( selectedLabel instanceof jQuery ) {
13895                 selectedLabel = selectedLabel.clone();
13896         }
13898         this.setLabel( selectedLabel );
13902  * Handle mouse click events.
13904  * @private
13905  * @param {jQuery.Event} e Mouse click event
13906  */
13907 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
13908         if ( !this.isDisabled() && e.which === 1 ) {
13909                 this.menu.toggle();
13910         }
13911         return false;
13915  * Handle key press events.
13917  * @private
13918  * @param {jQuery.Event} e Key press event
13919  */
13920 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
13921         if ( !this.isDisabled() &&
13922                 ( ( e.which === OO.ui.Keys.SPACE && !this.menu.isVisible() ) || e.which === OO.ui.Keys.ENTER )
13923         ) {
13924                 this.menu.toggle();
13925                 return false;
13926         }
13930  * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
13931  * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
13932  * OO.ui.mixin.IndicatorElement indicators}.
13933  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13935  *     @example
13936  *     // Example of a file select widget
13937  *     var selectFile = new OO.ui.SelectFileWidget();
13938  *     $( 'body' ).append( selectFile.$element );
13940  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
13942  * @class
13943  * @extends OO.ui.Widget
13944  * @mixins OO.ui.mixin.IconElement
13945  * @mixins OO.ui.mixin.IndicatorElement
13946  * @mixins OO.ui.mixin.PendingElement
13947  * @mixins OO.ui.mixin.LabelElement
13949  * @constructor
13950  * @param {Object} [config] Configuration options
13951  * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
13952  * @cfg {string} [placeholder] Text to display when no file is selected.
13953  * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
13954  * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
13955  * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be true.
13956  * @cfg {boolean} [dragDropUI=false] Deprecated alias for showDropTarget
13957  */
13958 OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
13959         var dragHandler;
13961         // TODO: Remove in next release
13962         if ( config && config.dragDropUI ) {
13963                 config.showDropTarget = true;
13964         }
13966         // Configuration initialization
13967         config = $.extend( {
13968                 accept: null,
13969                 placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
13970                 notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
13971                 droppable: true,
13972                 showDropTarget: false
13973         }, config );
13975         // Parent constructor
13976         OO.ui.SelectFileWidget.parent.call( this, config );
13978         // Mixin constructors
13979         OO.ui.mixin.IconElement.call( this, config );
13980         OO.ui.mixin.IndicatorElement.call( this, config );
13981         OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$info } ) );
13982         OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { autoFitLabel: true } ) );
13984         // Properties
13985         this.$info = $( '<span>' );
13987         // Properties
13988         this.showDropTarget = config.showDropTarget;
13989         this.isSupported = this.constructor.static.isSupported();
13990         this.currentFile = null;
13991         if ( Array.isArray( config.accept ) ) {
13992                 this.accept = config.accept;
13993         } else {
13994                 this.accept = null;
13995         }
13996         this.placeholder = config.placeholder;
13997         this.notsupported = config.notsupported;
13998         this.onFileSelectedHandler = this.onFileSelected.bind( this );
14000         this.selectButton = new OO.ui.ButtonWidget( {
14001                 classes: [ 'oo-ui-selectFileWidget-selectButton' ],
14002                 label: 'Select a file',
14003                 disabled: this.disabled || !this.isSupported
14004         } );
14006         this.clearButton = new OO.ui.ButtonWidget( {
14007                 classes: [ 'oo-ui-selectFileWidget-clearButton' ],
14008                 framed: false,
14009                 icon: 'remove',
14010                 disabled: this.disabled
14011         } );
14013         // Events
14014         this.selectButton.$button.on( {
14015                 keypress: this.onKeyPress.bind( this )
14016         } );
14017         this.clearButton.connect( this, {
14018                 click: 'onClearClick'
14019         } );
14020         if ( config.droppable ) {
14021                 dragHandler = this.onDragEnterOrOver.bind( this );
14022                 this.$element.on( {
14023                         dragenter: dragHandler,
14024                         dragover: dragHandler,
14025                         dragleave: this.onDragLeave.bind( this ),
14026                         drop: this.onDrop.bind( this )
14027                 } );
14028         }
14030         // Initialization
14031         this.addInput();
14032         this.updateUI();
14033         this.$label.addClass( 'oo-ui-selectFileWidget-label' );
14034         this.$info
14035                 .addClass( 'oo-ui-selectFileWidget-info' )
14036                 .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
14037         this.$element
14038                 .addClass( 'oo-ui-selectFileWidget' )
14039                 .append( this.$info, this.selectButton.$element );
14040         if ( config.droppable && config.showDropTarget ) {
14041                 this.$dropTarget = $( '<div>' )
14042                         .addClass( 'oo-ui-selectFileWidget-dropTarget' )
14043                         .text( OO.ui.msg( 'ooui-selectfile-dragdrop-placeholder' ) )
14044                         .on( {
14045                                 click: this.onDropTargetClick.bind( this )
14046                         } );
14047                 this.$element.prepend( this.$dropTarget );
14048         }
14051 /* Setup */
14053 OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
14054 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
14055 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
14056 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
14057 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
14059 /* Static Properties */
14062  * Check if this widget is supported
14064  * @static
14065  * @return {boolean}
14066  */
14067 OO.ui.SelectFileWidget.static.isSupported = function () {
14068         var $input;
14069         if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
14070                 $input = $( '<input type="file">' );
14071                 OO.ui.SelectFileWidget.static.isSupportedCache = $input[ 0 ].files !== undefined;
14072         }
14073         return OO.ui.SelectFileWidget.static.isSupportedCache;
14076 OO.ui.SelectFileWidget.static.isSupportedCache = null;
14078 /* Events */
14081  * @event change
14083  * A change event is emitted when the on/off state of the toggle changes.
14085  * @param {File|null} value New value
14086  */
14088 /* Methods */
14091  * Get the current value of the field
14093  * @return {File|null}
14094  */
14095 OO.ui.SelectFileWidget.prototype.getValue = function () {
14096         return this.currentFile;
14100  * Set the current value of the field
14102  * @param {File|null} file File to select
14103  */
14104 OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
14105         if ( this.currentFile !== file ) {
14106                 this.currentFile = file;
14107                 this.updateUI();
14108                 this.emit( 'change', this.currentFile );
14109         }
14113  * Update the user interface when a file is selected or unselected
14115  * @protected
14116  */
14117 OO.ui.SelectFileWidget.prototype.updateUI = function () {
14118         var $label;
14119         if ( !this.isSupported ) {
14120                 this.$element.addClass( 'oo-ui-selectFileWidget-notsupported' );
14121                 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
14122                 this.setLabel( this.notsupported );
14123         } else {
14124                 this.$element.addClass( 'oo-ui-selectFileWidget-supported' );
14125                 if ( this.currentFile ) {
14126                         this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
14127                         $label = $( [] );
14128                         if ( this.currentFile.type !== '' ) {
14129                                 $label = $label.add( $( '<span>' ).addClass( 'oo-ui-selectFileWidget-fileType' ).text( this.currentFile.type ) );
14130                         }
14131                         $label = $label.add( $( '<span>' ).text( this.currentFile.name ) );
14132                         this.setLabel( $label );
14133                 } else {
14134                         this.$element.addClass( 'oo-ui-selectFileWidget-empty' );
14135                         this.setLabel( this.placeholder );
14136                 }
14137         }
14139         if ( this.$input ) {
14140                 this.$input.attr( 'title', this.getLabel() );
14141         }
14145  * Add the input to the widget
14147  * @private
14148  */
14149 OO.ui.SelectFileWidget.prototype.addInput = function () {
14150         if ( this.$input ) {
14151                 this.$input.remove();
14152         }
14154         if ( !this.isSupported ) {
14155                 this.$input = null;
14156                 return;
14157         }
14159         this.$input = $( '<input type="file">' );
14160         this.$input.on( 'change', this.onFileSelectedHandler );
14161         this.$input.attr( {
14162                 tabindex: -1,
14163                 title: this.getLabel()
14164         } );
14165         if ( this.accept ) {
14166                 this.$input.attr( 'accept', this.accept.join( ', ' ) );
14167         }
14168         this.selectButton.$button.append( this.$input );
14172  * Determine if we should accept this file
14174  * @private
14175  * @param {string} File MIME type
14176  * @return {boolean}
14177  */
14178 OO.ui.SelectFileWidget.prototype.isAllowedType = function ( mimeType ) {
14179         var i, mimeTest;
14181         if ( !this.accept || !mimeType ) {
14182                 return true;
14183         }
14185         for ( i = 0; i < this.accept.length; i++ ) {
14186                 mimeTest = this.accept[ i ];
14187                 if ( mimeTest === mimeType ) {
14188                         return true;
14189                 } else if ( mimeTest.substr( -2 ) === '/*' ) {
14190                         mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
14191                         if ( mimeType.substr( 0, mimeTest.length ) === mimeTest ) {
14192                                 return true;
14193                         }
14194                 }
14195         }
14197         return false;
14201  * Handle file selection from the input
14203  * @private
14204  * @param {jQuery.Event} e
14205  */
14206 OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
14207         var file = OO.getProp( e.target, 'files', 0 ) || null;
14209         if ( file && !this.isAllowedType( file.type ) ) {
14210                 file = null;
14211         }
14213         this.setValue( file );
14214         this.addInput();
14218  * Handle clear button click events.
14220  * @private
14221  */
14222 OO.ui.SelectFileWidget.prototype.onClearClick = function () {
14223         this.setValue( null );
14224         return false;
14228  * Handle key press events.
14230  * @private
14231  * @param {jQuery.Event} e Key press event
14232  */
14233 OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
14234         if ( this.isSupported && !this.isDisabled() && this.$input &&
14235                 ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
14236         ) {
14237                 this.$input.click();
14238                 return false;
14239         }
14243  * Handle drop target click events.
14245  * @private
14246  * @param {jQuery.Event} e Key press event
14247  */
14248 OO.ui.SelectFileWidget.prototype.onDropTargetClick = function () {
14249         if ( this.isSupported && !this.isDisabled() && this.$input ) {
14250                 this.$input.click();
14251                 return false;
14252         }
14256  * Handle drag enter and over events
14258  * @private
14259  * @param {jQuery.Event} e Drag event
14260  */
14261 OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
14262         var itemOrFile,
14263                 droppableFile = false,
14264                 dt = e.originalEvent.dataTransfer;
14266         e.preventDefault();
14267         e.stopPropagation();
14269         if ( this.isDisabled() || !this.isSupported ) {
14270                 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
14271                 dt.dropEffect = 'none';
14272                 return false;
14273         }
14275         // DataTransferItem and File both have a type property, but in Chrome files
14276         // have no information at this point.
14277         itemOrFile = OO.getProp( dt, 'items', 0 ) || OO.getProp( dt, 'files', 0 );
14278         if ( itemOrFile ) {
14279                 if ( this.isAllowedType( itemOrFile.type ) ) {
14280                         droppableFile = true;
14281                 }
14282         // dt.types is Array-like, but not an Array
14283         } else if ( Array.prototype.indexOf.call( OO.getProp( dt, 'types' ) || [], 'Files' ) !== -1 ) {
14284                 // File information is not available at this point for security so just assume
14285                 // it is acceptable for now.
14286                 // https://bugzilla.mozilla.org/show_bug.cgi?id=640534
14287                 droppableFile = true;
14288         }
14290         this.$element.toggleClass( 'oo-ui-selectFileWidget-canDrop', droppableFile );
14291         if ( !droppableFile ) {
14292                 dt.dropEffect = 'none';
14293         }
14295         return false;
14299  * Handle drag leave events
14301  * @private
14302  * @param {jQuery.Event} e Drag event
14303  */
14304 OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
14305         this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
14309  * Handle drop events
14311  * @private
14312  * @param {jQuery.Event} e Drop event
14313  */
14314 OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
14315         var file = null,
14316                 dt = e.originalEvent.dataTransfer;
14318         e.preventDefault();
14319         e.stopPropagation();
14320         this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
14322         if ( this.isDisabled() || !this.isSupported ) {
14323                 return false;
14324         }
14326         file = OO.getProp( dt, 'files', 0 );
14327         if ( file && !this.isAllowedType( file.type ) ) {
14328                 file = null;
14329         }
14330         if ( file ) {
14331                 this.setValue( file );
14332         }
14334         return false;
14338  * @inheritdoc
14339  */
14340 OO.ui.SelectFileWidget.prototype.setDisabled = function ( disabled ) {
14341         OO.ui.SelectFileWidget.parent.prototype.setDisabled.call( this, disabled );
14342         if ( this.selectButton ) {
14343                 this.selectButton.setDisabled( disabled );
14344         }
14345         if ( this.clearButton ) {
14346                 this.clearButton.setDisabled( disabled );
14347         }
14348         return this;
14352  * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
14353  * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
14354  * for a list of icons included in the library.
14356  *     @example
14357  *     // An icon widget with a label
14358  *     var myIcon = new OO.ui.IconWidget( {
14359  *         icon: 'help',
14360  *         iconTitle: 'Help'
14361  *      } );
14362  *      // Create a label.
14363  *      var iconLabel = new OO.ui.LabelWidget( {
14364  *          label: 'Help'
14365  *      } );
14366  *      $( 'body' ).append( myIcon.$element, iconLabel.$element );
14368  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
14370  * @class
14371  * @extends OO.ui.Widget
14372  * @mixins OO.ui.mixin.IconElement
14373  * @mixins OO.ui.mixin.TitledElement
14374  * @mixins OO.ui.mixin.FlaggedElement
14376  * @constructor
14377  * @param {Object} [config] Configuration options
14378  */
14379 OO.ui.IconWidget = function OoUiIconWidget( config ) {
14380         // Configuration initialization
14381         config = config || {};
14383         // Parent constructor
14384         OO.ui.IconWidget.parent.call( this, config );
14386         // Mixin constructors
14387         OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
14388         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
14389         OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
14391         // Initialization
14392         this.$element.addClass( 'oo-ui-iconWidget' );
14395 /* Setup */
14397 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
14398 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
14399 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
14400 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
14402 /* Static Properties */
14404 OO.ui.IconWidget.static.tagName = 'span';
14407  * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
14408  * attention to the status of an item or to clarify the function of a control. For a list of
14409  * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
14411  *     @example
14412  *     // Example of an indicator widget
14413  *     var indicator1 = new OO.ui.IndicatorWidget( {
14414  *         indicator: 'alert'
14415  *     } );
14417  *     // Create a fieldset layout to add a label
14418  *     var fieldset = new OO.ui.FieldsetLayout();
14419  *     fieldset.addItems( [
14420  *         new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
14421  *     ] );
14422  *     $( 'body' ).append( fieldset.$element );
14424  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
14426  * @class
14427  * @extends OO.ui.Widget
14428  * @mixins OO.ui.mixin.IndicatorElement
14429  * @mixins OO.ui.mixin.TitledElement
14431  * @constructor
14432  * @param {Object} [config] Configuration options
14433  */
14434 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
14435         // Configuration initialization
14436         config = config || {};
14438         // Parent constructor
14439         OO.ui.IndicatorWidget.parent.call( this, config );
14441         // Mixin constructors
14442         OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
14443         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
14445         // Initialization
14446         this.$element.addClass( 'oo-ui-indicatorWidget' );
14449 /* Setup */
14451 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
14452 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
14453 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
14455 /* Static Properties */
14457 OO.ui.IndicatorWidget.static.tagName = 'span';
14460  * InputWidget is the base class for all input widgets, which
14461  * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
14462  * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
14463  * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
14465  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
14467  * @abstract
14468  * @class
14469  * @extends OO.ui.Widget
14470  * @mixins OO.ui.mixin.FlaggedElement
14471  * @mixins OO.ui.mixin.TabIndexedElement
14472  * @mixins OO.ui.mixin.TitledElement
14473  * @mixins OO.ui.mixin.AccessKeyedElement
14475  * @constructor
14476  * @param {Object} [config] Configuration options
14477  * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
14478  * @cfg {string} [value=''] The value of the input.
14479  * @cfg {string} [accessKey=''] The access key of the input.
14480  * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
14481  *  before it is accepted.
14482  */
14483 OO.ui.InputWidget = function OoUiInputWidget( config ) {
14484         // Configuration initialization
14485         config = config || {};
14487         // Parent constructor
14488         OO.ui.InputWidget.parent.call( this, config );
14490         // Properties
14491         this.$input = this.getInputElement( config );
14492         this.value = '';
14493         this.inputFilter = config.inputFilter;
14495         // Mixin constructors
14496         OO.ui.mixin.FlaggedElement.call( this, config );
14497         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
14498         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
14499         OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$input } ) );
14501         // Events
14502         this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
14504         // Initialization
14505         this.$input
14506                 .addClass( 'oo-ui-inputWidget-input' )
14507                 .attr( 'name', config.name )
14508                 .prop( 'disabled', this.isDisabled() );
14509         this.$element
14510                 .addClass( 'oo-ui-inputWidget' )
14511                 .append( this.$input );
14512         this.setValue( config.value );
14513         this.setAccessKey( config.accessKey );
14516 /* Setup */
14518 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
14519 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
14520 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
14521 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
14522 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
14524 /* Static Properties */
14526 OO.ui.InputWidget.static.supportsSimpleLabel = true;
14528 /* Events */
14531  * @event change
14533  * A change event is emitted when the value of the input changes.
14535  * @param {string} value
14536  */
14538 /* Methods */
14541  * Get input element.
14543  * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
14544  * different circumstances. The element must have a `value` property (like form elements).
14546  * @protected
14547  * @param {Object} config Configuration options
14548  * @return {jQuery} Input element
14549  */
14550 OO.ui.InputWidget.prototype.getInputElement = function () {
14551         return $( '<input>' );
14555  * Handle potentially value-changing events.
14557  * @private
14558  * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
14559  */
14560 OO.ui.InputWidget.prototype.onEdit = function () {
14561         var widget = this;
14562         if ( !this.isDisabled() ) {
14563                 // Allow the stack to clear so the value will be updated
14564                 setTimeout( function () {
14565                         widget.setValue( widget.$input.val() );
14566                 } );
14567         }
14571  * Get the value of the input.
14573  * @return {string} Input value
14574  */
14575 OO.ui.InputWidget.prototype.getValue = function () {
14576         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
14577         // it, and we won't know unless they're kind enough to trigger a 'change' event.
14578         var value = this.$input.val();
14579         if ( this.value !== value ) {
14580                 this.setValue( value );
14581         }
14582         return this.value;
14586  * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right).
14588  * @param {boolean} isRTL
14589  * Direction is right-to-left
14590  */
14591 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
14592         this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
14596  * Set the value of the input.
14598  * @param {string} value New value
14599  * @fires change
14600  * @chainable
14601  */
14602 OO.ui.InputWidget.prototype.setValue = function ( value ) {
14603         value = this.cleanUpValue( value );
14604         // Update the DOM if it has changed. Note that with cleanUpValue, it
14605         // is possible for the DOM value to change without this.value changing.
14606         if ( this.$input.val() !== value ) {
14607                 this.$input.val( value );
14608         }
14609         if ( this.value !== value ) {
14610                 this.value = value;
14611                 this.emit( 'change', this.value );
14612         }
14613         return this;
14617  * Set the input's access key.
14618  * FIXME: This is the same code as in OO.ui.mixin.ButtonElement, maybe find a better place for it?
14620  * @param {string} accessKey Input's access key, use empty string to remove
14621  * @chainable
14622  */
14623 OO.ui.InputWidget.prototype.setAccessKey = function ( accessKey ) {
14624         accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
14626         if ( this.accessKey !== accessKey ) {
14627                 if ( this.$input ) {
14628                         if ( accessKey !== null ) {
14629                                 this.$input.attr( 'accesskey', accessKey );
14630                         } else {
14631                                 this.$input.removeAttr( 'accesskey' );
14632                         }
14633                 }
14634                 this.accessKey = accessKey;
14635         }
14637         return this;
14641  * Clean up incoming value.
14643  * Ensures value is a string, and converts undefined and null to empty string.
14645  * @private
14646  * @param {string} value Original value
14647  * @return {string} Cleaned up value
14648  */
14649 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
14650         if ( value === undefined || value === null ) {
14651                 return '';
14652         } else if ( this.inputFilter ) {
14653                 return this.inputFilter( String( value ) );
14654         } else {
14655                 return String( value );
14656         }
14660  * Simulate the behavior of clicking on a label bound to this input. This method is only called by
14661  * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
14662  * called directly.
14663  */
14664 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
14665         if ( !this.isDisabled() ) {
14666                 if ( this.$input.is( ':checkbox, :radio' ) ) {
14667                         this.$input.click();
14668                 }
14669                 if ( this.$input.is( ':input' ) ) {
14670                         this.$input[ 0 ].focus();
14671                 }
14672         }
14676  * @inheritdoc
14677  */
14678 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
14679         OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
14680         if ( this.$input ) {
14681                 this.$input.prop( 'disabled', this.isDisabled() );
14682         }
14683         return this;
14687  * Focus the input.
14689  * @chainable
14690  */
14691 OO.ui.InputWidget.prototype.focus = function () {
14692         this.$input[ 0 ].focus();
14693         return this;
14697  * Blur the input.
14699  * @chainable
14700  */
14701 OO.ui.InputWidget.prototype.blur = function () {
14702         this.$input[ 0 ].blur();
14703         return this;
14707  * @inheritdoc
14708  */
14709 OO.ui.InputWidget.prototype.gatherPreInfuseState = function ( node ) {
14710         var
14711                 state = OO.ui.InputWidget.parent.prototype.gatherPreInfuseState.call( this, node ),
14712                 $input = state.$input || $( node ).find( '.oo-ui-inputWidget-input' );
14713         state.value = $input.val();
14714         // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
14715         state.focus = $input.is( ':focus' );
14716         return state;
14720  * @inheritdoc
14721  */
14722 OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
14723         OO.ui.InputWidget.parent.prototype.restorePreInfuseState.call( this, state );
14724         if ( state.value !== undefined && state.value !== this.getValue() ) {
14725                 this.setValue( state.value );
14726         }
14727         if ( state.focus ) {
14728                 this.focus();
14729         }
14733  * ButtonInputWidget is used to submit HTML forms and is intended to be used within
14734  * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
14735  * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
14736  * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
14737  * [OOjs UI documentation on MediaWiki] [1] for more information.
14739  *     @example
14740  *     // A ButtonInputWidget rendered as an HTML button, the default.
14741  *     var button = new OO.ui.ButtonInputWidget( {
14742  *         label: 'Input button',
14743  *         icon: 'check',
14744  *         value: 'check'
14745  *     } );
14746  *     $( 'body' ).append( button.$element );
14748  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
14750  * @class
14751  * @extends OO.ui.InputWidget
14752  * @mixins OO.ui.mixin.ButtonElement
14753  * @mixins OO.ui.mixin.IconElement
14754  * @mixins OO.ui.mixin.IndicatorElement
14755  * @mixins OO.ui.mixin.LabelElement
14756  * @mixins OO.ui.mixin.TitledElement
14758  * @constructor
14759  * @param {Object} [config] Configuration options
14760  * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
14761  * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
14762  *  Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
14763  *  non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
14764  *  be set to `true` when there’s need to support IE6 in a form with multiple buttons.
14765  */
14766 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
14767         // Configuration initialization
14768         config = $.extend( { type: 'button', useInputTag: false }, config );
14770         // Properties (must be set before parent constructor, which calls #setValue)
14771         this.useInputTag = config.useInputTag;
14773         // Parent constructor
14774         OO.ui.ButtonInputWidget.parent.call( this, config );
14776         // Mixin constructors
14777         OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
14778         OO.ui.mixin.IconElement.call( this, config );
14779         OO.ui.mixin.IndicatorElement.call( this, config );
14780         OO.ui.mixin.LabelElement.call( this, config );
14781         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
14783         // Initialization
14784         if ( !config.useInputTag ) {
14785                 this.$input.append( this.$icon, this.$label, this.$indicator );
14786         }
14787         this.$element.addClass( 'oo-ui-buttonInputWidget' );
14790 /* Setup */
14792 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
14793 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
14794 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
14795 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
14796 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
14797 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
14799 /* Static Properties */
14802  * Disable generating `<label>` elements for buttons. One would very rarely need additional label
14803  * for a button, and it's already a big clickable target, and it causes unexpected rendering.
14804  */
14805 OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
14807 /* Methods */
14810  * @inheritdoc
14811  * @protected
14812  */
14813 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
14814         var type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ?
14815                 config.type :
14816                 'button';
14817         return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
14821  * Set label value.
14823  * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
14825  * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
14826  *  text, or `null` for no label
14827  * @chainable
14828  */
14829 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
14830         OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
14832         if ( this.useInputTag ) {
14833                 if ( typeof label === 'function' ) {
14834                         label = OO.ui.resolveMsg( label );
14835                 }
14836                 if ( label instanceof jQuery ) {
14837                         label = label.text();
14838                 }
14839                 if ( !label ) {
14840                         label = '';
14841                 }
14842                 this.$input.val( label );
14843         }
14845         return this;
14849  * Set the value of the input.
14851  * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
14852  * they do not support {@link #value values}.
14854  * @param {string} value New value
14855  * @chainable
14856  */
14857 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
14858         if ( !this.useInputTag ) {
14859                 OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
14860         }
14861         return this;
14865  * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
14866  * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
14867  * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
14868  * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
14870  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
14872  *     @example
14873  *     // An example of selected, unselected, and disabled checkbox inputs
14874  *     var checkbox1=new OO.ui.CheckboxInputWidget( {
14875  *          value: 'a',
14876  *          selected: true
14877  *     } );
14878  *     var checkbox2=new OO.ui.CheckboxInputWidget( {
14879  *         value: 'b'
14880  *     } );
14881  *     var checkbox3=new OO.ui.CheckboxInputWidget( {
14882  *         value:'c',
14883  *         disabled: true
14884  *     } );
14885  *     // Create a fieldset layout with fields for each checkbox.
14886  *     var fieldset = new OO.ui.FieldsetLayout( {
14887  *         label: 'Checkboxes'
14888  *     } );
14889  *     fieldset.addItems( [
14890  *         new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
14891  *         new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
14892  *         new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
14893  *     ] );
14894  *     $( 'body' ).append( fieldset.$element );
14896  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
14898  * @class
14899  * @extends OO.ui.InputWidget
14901  * @constructor
14902  * @param {Object} [config] Configuration options
14903  * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
14904  */
14905 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
14906         // Configuration initialization
14907         config = config || {};
14909         // Parent constructor
14910         OO.ui.CheckboxInputWidget.parent.call( this, config );
14912         // Initialization
14913         this.$element
14914                 .addClass( 'oo-ui-checkboxInputWidget' )
14915                 // Required for pretty styling in MediaWiki theme
14916                 .append( $( '<span>' ) );
14917         this.setSelected( config.selected !== undefined ? config.selected : false );
14920 /* Setup */
14922 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
14924 /* Methods */
14927  * @inheritdoc
14928  * @protected
14929  */
14930 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
14931         return $( '<input type="checkbox" />' );
14935  * @inheritdoc
14936  */
14937 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
14938         var widget = this;
14939         if ( !this.isDisabled() ) {
14940                 // Allow the stack to clear so the value will be updated
14941                 setTimeout( function () {
14942                         widget.setSelected( widget.$input.prop( 'checked' ) );
14943                 } );
14944         }
14948  * Set selection state of this checkbox.
14950  * @param {boolean} state `true` for selected
14951  * @chainable
14952  */
14953 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
14954         state = !!state;
14955         if ( this.selected !== state ) {
14956                 this.selected = state;
14957                 this.$input.prop( 'checked', this.selected );
14958                 this.emit( 'change', this.selected );
14959         }
14960         return this;
14964  * Check if this checkbox is selected.
14966  * @return {boolean} Checkbox is selected
14967  */
14968 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
14969         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
14970         // it, and we won't know unless they're kind enough to trigger a 'change' event.
14971         var selected = this.$input.prop( 'checked' );
14972         if ( this.selected !== selected ) {
14973                 this.setSelected( selected );
14974         }
14975         return this.selected;
14979  * @inheritdoc
14980  */
14981 OO.ui.CheckboxInputWidget.prototype.gatherPreInfuseState = function ( node ) {
14982         var
14983                 state = OO.ui.CheckboxInputWidget.parent.prototype.gatherPreInfuseState.call( this, node ),
14984                 $input = $( node ).find( '.oo-ui-inputWidget-input' );
14985         state.$input = $input; // shortcut for performance, used in InputWidget
14986         state.checked = $input.prop( 'checked' );
14987         return state;
14991  * @inheritdoc
14992  */
14993 OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
14994         OO.ui.CheckboxInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
14995         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
14996                 this.setSelected( state.checked );
14997         }
15001  * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
15002  * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
15003  * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
15004  * more information about input widgets.
15006  * A DropdownInputWidget always has a value (one of the options is always selected), unless there
15007  * are no options. If no `value` configuration option is provided, the first option is selected.
15008  * If you need a state representing no value (no option being selected), use a DropdownWidget.
15010  * This and OO.ui.RadioSelectInputWidget support the same configuration options.
15012  *     @example
15013  *     // Example: A DropdownInputWidget with three options
15014  *     var dropdownInput = new OO.ui.DropdownInputWidget( {
15015  *         options: [
15016  *             { data: 'a', label: 'First' },
15017  *             { data: 'b', label: 'Second'},
15018  *             { data: 'c', label: 'Third' }
15019  *         ]
15020  *     } );
15021  *     $( 'body' ).append( dropdownInput.$element );
15023  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15025  * @class
15026  * @extends OO.ui.InputWidget
15027  * @mixins OO.ui.mixin.TitledElement
15029  * @constructor
15030  * @param {Object} [config] Configuration options
15031  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
15032  * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
15033  */
15034 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
15035         // Configuration initialization
15036         config = config || {};
15038         // Properties (must be done before parent constructor which calls #setDisabled)
15039         this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
15041         // Parent constructor
15042         OO.ui.DropdownInputWidget.parent.call( this, config );
15044         // Mixin constructors
15045         OO.ui.mixin.TitledElement.call( this, config );
15047         // Events
15048         this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
15050         // Initialization
15051         this.setOptions( config.options || [] );
15052         this.$element
15053                 .addClass( 'oo-ui-dropdownInputWidget' )
15054                 .append( this.dropdownWidget.$element );
15057 /* Setup */
15059 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
15060 OO.mixinClass( OO.ui.DropdownInputWidget, OO.ui.mixin.TitledElement );
15062 /* Methods */
15065  * @inheritdoc
15066  * @protected
15067  */
15068 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
15069         return $( '<input type="hidden">' );
15073  * Handles menu select events.
15075  * @private
15076  * @param {OO.ui.MenuOptionWidget} item Selected menu item
15077  */
15078 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
15079         this.setValue( item.getData() );
15083  * @inheritdoc
15084  */
15085 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
15086         value = this.cleanUpValue( value );
15087         this.dropdownWidget.getMenu().selectItemByData( value );
15088         OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
15089         return this;
15093  * @inheritdoc
15094  */
15095 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
15096         this.dropdownWidget.setDisabled( state );
15097         OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
15098         return this;
15102  * Set the options available for this input.
15104  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
15105  * @chainable
15106  */
15107 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
15108         var
15109                 value = this.getValue(),
15110                 widget = this;
15112         // Rebuild the dropdown menu
15113         this.dropdownWidget.getMenu()
15114                 .clearItems()
15115                 .addItems( options.map( function ( opt ) {
15116                         var optValue = widget.cleanUpValue( opt.data );
15117                         return new OO.ui.MenuOptionWidget( {
15118                                 data: optValue,
15119                                 label: opt.label !== undefined ? opt.label : optValue
15120                         } );
15121                 } ) );
15123         // Restore the previous value, or reset to something sensible
15124         if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
15125                 // Previous value is still available, ensure consistency with the dropdown
15126                 this.setValue( value );
15127         } else {
15128                 // No longer valid, reset
15129                 if ( options.length ) {
15130                         this.setValue( options[ 0 ].data );
15131                 }
15132         }
15134         return this;
15138  * @inheritdoc
15139  */
15140 OO.ui.DropdownInputWidget.prototype.focus = function () {
15141         this.dropdownWidget.getMenu().toggle( true );
15142         return this;
15146  * @inheritdoc
15147  */
15148 OO.ui.DropdownInputWidget.prototype.blur = function () {
15149         this.dropdownWidget.getMenu().toggle( false );
15150         return this;
15154  * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
15155  * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
15156  * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
15157  * please see the [OOjs UI documentation on MediaWiki][1].
15159  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
15161  *     @example
15162  *     // An example of selected, unselected, and disabled radio inputs
15163  *     var radio1 = new OO.ui.RadioInputWidget( {
15164  *         value: 'a',
15165  *         selected: true
15166  *     } );
15167  *     var radio2 = new OO.ui.RadioInputWidget( {
15168  *         value: 'b'
15169  *     } );
15170  *     var radio3 = new OO.ui.RadioInputWidget( {
15171  *         value: 'c',
15172  *         disabled: true
15173  *     } );
15174  *     // Create a fieldset layout with fields for each radio button.
15175  *     var fieldset = new OO.ui.FieldsetLayout( {
15176  *         label: 'Radio inputs'
15177  *     } );
15178  *     fieldset.addItems( [
15179  *         new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
15180  *         new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
15181  *         new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
15182  *     ] );
15183  *     $( 'body' ).append( fieldset.$element );
15185  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15187  * @class
15188  * @extends OO.ui.InputWidget
15190  * @constructor
15191  * @param {Object} [config] Configuration options
15192  * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
15193  */
15194 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
15195         // Configuration initialization
15196         config = config || {};
15198         // Parent constructor
15199         OO.ui.RadioInputWidget.parent.call( this, config );
15201         // Initialization
15202         this.$element
15203                 .addClass( 'oo-ui-radioInputWidget' )
15204                 // Required for pretty styling in MediaWiki theme
15205                 .append( $( '<span>' ) );
15206         this.setSelected( config.selected !== undefined ? config.selected : false );
15209 /* Setup */
15211 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
15213 /* Methods */
15216  * @inheritdoc
15217  * @protected
15218  */
15219 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
15220         return $( '<input type="radio" />' );
15224  * @inheritdoc
15225  */
15226 OO.ui.RadioInputWidget.prototype.onEdit = function () {
15227         // RadioInputWidget doesn't track its state.
15231  * Set selection state of this radio button.
15233  * @param {boolean} state `true` for selected
15234  * @chainable
15235  */
15236 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
15237         // RadioInputWidget doesn't track its state.
15238         this.$input.prop( 'checked', state );
15239         return this;
15243  * Check if this radio button is selected.
15245  * @return {boolean} Radio is selected
15246  */
15247 OO.ui.RadioInputWidget.prototype.isSelected = function () {
15248         return this.$input.prop( 'checked' );
15252  * @inheritdoc
15253  */
15254 OO.ui.RadioInputWidget.prototype.gatherPreInfuseState = function ( node ) {
15255         var
15256                 state = OO.ui.RadioInputWidget.parent.prototype.gatherPreInfuseState.call( this, node ),
15257                 $input = $( node ).find( '.oo-ui-inputWidget-input' );
15258         state.$input = $input; // shortcut for performance, used in InputWidget
15259         state.checked = $input.prop( 'checked' );
15260         return state;
15264  * @inheritdoc
15265  */
15266 OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
15267         OO.ui.RadioInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
15268         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
15269                 this.setSelected( state.checked );
15270         }
15274  * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
15275  * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
15276  * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
15277  * more information about input widgets.
15279  * This and OO.ui.DropdownInputWidget support the same configuration options.
15281  *     @example
15282  *     // Example: A RadioSelectInputWidget with three options
15283  *     var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
15284  *         options: [
15285  *             { data: 'a', label: 'First' },
15286  *             { data: 'b', label: 'Second'},
15287  *             { data: 'c', label: 'Third' }
15288  *         ]
15289  *     } );
15290  *     $( 'body' ).append( radioSelectInput.$element );
15292  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15294  * @class
15295  * @extends OO.ui.InputWidget
15297  * @constructor
15298  * @param {Object} [config] Configuration options
15299  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
15300  */
15301 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
15302         // Configuration initialization
15303         config = config || {};
15305         // Properties (must be done before parent constructor which calls #setDisabled)
15306         this.radioSelectWidget = new OO.ui.RadioSelectWidget();
15308         // Parent constructor
15309         OO.ui.RadioSelectInputWidget.parent.call( this, config );
15311         // Events
15312         this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
15314         // Initialization
15315         this.setOptions( config.options || [] );
15316         this.$element
15317                 .addClass( 'oo-ui-radioSelectInputWidget' )
15318                 .append( this.radioSelectWidget.$element );
15321 /* Setup */
15323 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
15325 /* Static Properties */
15327 OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
15329 /* Methods */
15332  * @inheritdoc
15333  * @protected
15334  */
15335 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
15336         return $( '<input type="hidden">' );
15340  * Handles menu select events.
15342  * @private
15343  * @param {OO.ui.RadioOptionWidget} item Selected menu item
15344  */
15345 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
15346         this.setValue( item.getData() );
15350  * @inheritdoc
15351  */
15352 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
15353         value = this.cleanUpValue( value );
15354         this.radioSelectWidget.selectItemByData( value );
15355         OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
15356         return this;
15360  * @inheritdoc
15361  */
15362 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
15363         this.radioSelectWidget.setDisabled( state );
15364         OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
15365         return this;
15369  * Set the options available for this input.
15371  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
15372  * @chainable
15373  */
15374 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
15375         var
15376                 value = this.getValue(),
15377                 widget = this;
15379         // Rebuild the radioSelect menu
15380         this.radioSelectWidget
15381                 .clearItems()
15382                 .addItems( options.map( function ( opt ) {
15383                         var optValue = widget.cleanUpValue( opt.data );
15384                         return new OO.ui.RadioOptionWidget( {
15385                                 data: optValue,
15386                                 label: opt.label !== undefined ? opt.label : optValue
15387                         } );
15388                 } ) );
15390         // Restore the previous value, or reset to something sensible
15391         if ( this.radioSelectWidget.getItemFromData( value ) ) {
15392                 // Previous value is still available, ensure consistency with the radioSelect
15393                 this.setValue( value );
15394         } else {
15395                 // No longer valid, reset
15396                 if ( options.length ) {
15397                         this.setValue( options[ 0 ].data );
15398                 }
15399         }
15401         return this;
15405  * @inheritdoc
15406  */
15407 OO.ui.RadioSelectInputWidget.prototype.gatherPreInfuseState = function ( node ) {
15408         var state = OO.ui.RadioSelectInputWidget.parent.prototype.gatherPreInfuseState.call( this, node );
15409         state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
15410         return state;
15414  * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
15415  * size of the field as well as its presentation. In addition, these widgets can be configured
15416  * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
15417  * validation-pattern (used to determine if an input value is valid or not) and an input filter,
15418  * which modifies incoming values rather than validating them.
15419  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
15421  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
15423  *     @example
15424  *     // Example of a text input widget
15425  *     var textInput = new OO.ui.TextInputWidget( {
15426  *         value: 'Text input'
15427  *     } )
15428  *     $( 'body' ).append( textInput.$element );
15430  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15432  * @class
15433  * @extends OO.ui.InputWidget
15434  * @mixins OO.ui.mixin.IconElement
15435  * @mixins OO.ui.mixin.IndicatorElement
15436  * @mixins OO.ui.mixin.PendingElement
15437  * @mixins OO.ui.mixin.LabelElement
15439  * @constructor
15440  * @param {Object} [config] Configuration options
15441  * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
15442  *  'email' or 'url'. Ignored if `multiline` is true.
15444  *  Some values of `type` result in additional behaviors:
15446  *  - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
15447  *    empties the text field
15448  * @cfg {string} [placeholder] Placeholder text
15449  * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
15450  *  instruct the browser to focus this widget.
15451  * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
15452  * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
15453  * @cfg {boolean} [multiline=false] Allow multiple lines of text
15454  * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
15455  *  specifies minimum number of rows to display.
15456  * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
15457  *  Use the #maxRows config to specify a maximum number of displayed rows.
15458  * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
15459  *  Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
15460  * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
15461  *  the value or placeholder text: `'before'` or `'after'`
15462  * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
15463  * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
15464  * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
15465  *  pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
15466  *  (the value must contain only numbers); when RegExp, a regular expression that must match the
15467  *  value for it to be considered valid; when Function, a function receiving the value as parameter
15468  *  that must return true, or promise resolving to true, for it to be considered valid.
15469  */
15470 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
15471         // Configuration initialization
15472         config = $.extend( {
15473                 type: 'text',
15474                 labelPosition: 'after'
15475         }, config );
15476         if ( config.type === 'search' ) {
15477                 if ( config.icon === undefined ) {
15478                         config.icon = 'search';
15479                 }
15480                 // indicator: 'clear' is set dynamically later, depending on value
15481         }
15482         if ( config.required ) {
15483                 if ( config.indicator === undefined ) {
15484                         config.indicator = 'required';
15485                 }
15486         }
15488         // Parent constructor
15489         OO.ui.TextInputWidget.parent.call( this, config );
15491         // Mixin constructors
15492         OO.ui.mixin.IconElement.call( this, config );
15493         OO.ui.mixin.IndicatorElement.call( this, config );
15494         OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
15495         OO.ui.mixin.LabelElement.call( this, config );
15497         // Properties
15498         this.type = this.getSaneType( config );
15499         this.readOnly = false;
15500         this.multiline = !!config.multiline;
15501         this.autosize = !!config.autosize;
15502         this.minRows = config.rows !== undefined ? config.rows : '';
15503         this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
15504         this.validate = null;
15506         // Clone for resizing
15507         if ( this.autosize ) {
15508                 this.$clone = this.$input
15509                         .clone()
15510                         .insertAfter( this.$input )
15511                         .attr( 'aria-hidden', 'true' )
15512                         .addClass( 'oo-ui-element-hidden' );
15513         }
15515         this.setValidation( config.validate );
15516         this.setLabelPosition( config.labelPosition );
15518         // Events
15519         this.$input.on( {
15520                 keypress: this.onKeyPress.bind( this ),
15521                 blur: this.onBlur.bind( this )
15522         } );
15523         this.$input.one( {
15524                 focus: this.onElementAttach.bind( this )
15525         } );
15526         this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
15527         this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
15528         this.on( 'labelChange', this.updatePosition.bind( this ) );
15529         this.connect( this, {
15530                 change: 'onChange',
15531                 disable: 'onDisable'
15532         } );
15534         // Initialization
15535         this.$element
15536                 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
15537                 .append( this.$icon, this.$indicator );
15538         this.setReadOnly( !!config.readOnly );
15539         this.updateSearchIndicator();
15540         if ( config.placeholder ) {
15541                 this.$input.attr( 'placeholder', config.placeholder );
15542         }
15543         if ( config.maxLength !== undefined ) {
15544                 this.$input.attr( 'maxlength', config.maxLength );
15545         }
15546         if ( config.autofocus ) {
15547                 this.$input.attr( 'autofocus', 'autofocus' );
15548         }
15549         if ( config.required ) {
15550                 this.$input.attr( 'required', 'required' );
15551                 this.$input.attr( 'aria-required', 'true' );
15552         }
15553         if ( config.autocomplete === false ) {
15554                 this.$input.attr( 'autocomplete', 'off' );
15555                 // Turning off autocompletion also disables "form caching" when the user navigates to a
15556                 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
15557                 $( window ).on( {
15558                         beforeunload: function () {
15559                                 this.$input.removeAttr( 'autocomplete' );
15560                         }.bind( this ),
15561                         pageshow: function () {
15562                                 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
15563                                 // whole page... it shouldn't hurt, though.
15564                                 this.$input.attr( 'autocomplete', 'off' );
15565                         }.bind( this )
15566                 } );
15567         }
15568         if ( this.multiline && config.rows ) {
15569                 this.$input.attr( 'rows', config.rows );
15570         }
15571         if ( this.label || config.autosize ) {
15572                 this.installParentChangeDetector();
15573         }
15576 /* Setup */
15578 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
15579 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
15580 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
15581 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
15582 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
15584 /* Static Properties */
15586 OO.ui.TextInputWidget.static.validationPatterns = {
15587         'non-empty': /.+/,
15588         integer: /^\d+$/
15591 /* Events */
15594  * An `enter` event is emitted when the user presses 'enter' inside the text box.
15596  * Not emitted if the input is multiline.
15598  * @event enter
15599  */
15601 /* Methods */
15604  * Handle icon mouse down events.
15606  * @private
15607  * @param {jQuery.Event} e Mouse down event
15608  * @fires icon
15609  */
15610 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
15611         if ( e.which === 1 ) {
15612                 this.$input[ 0 ].focus();
15613                 return false;
15614         }
15618  * Handle indicator mouse down events.
15620  * @private
15621  * @param {jQuery.Event} e Mouse down event
15622  * @fires indicator
15623  */
15624 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
15625         if ( e.which === 1 ) {
15626                 if ( this.type === 'search' ) {
15627                         // Clear the text field
15628                         this.setValue( '' );
15629                 }
15630                 this.$input[ 0 ].focus();
15631                 return false;
15632         }
15636  * Handle key press events.
15638  * @private
15639  * @param {jQuery.Event} e Key press event
15640  * @fires enter If enter key is pressed and input is not multiline
15641  */
15642 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
15643         if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
15644                 this.emit( 'enter', e );
15645         }
15649  * Handle blur events.
15651  * @private
15652  * @param {jQuery.Event} e Blur event
15653  */
15654 OO.ui.TextInputWidget.prototype.onBlur = function () {
15655         this.setValidityFlag();
15659  * Handle element attach events.
15661  * @private
15662  * @param {jQuery.Event} e Element attach event
15663  */
15664 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
15665         // Any previously calculated size is now probably invalid if we reattached elsewhere
15666         this.valCache = null;
15667         this.adjustSize();
15668         this.positionLabel();
15672  * Handle change events.
15674  * @param {string} value
15675  * @private
15676  */
15677 OO.ui.TextInputWidget.prototype.onChange = function () {
15678         this.updateSearchIndicator();
15679         this.setValidityFlag();
15680         this.adjustSize();
15684  * Handle disable events.
15686  * @param {boolean} disabled Element is disabled
15687  * @private
15688  */
15689 OO.ui.TextInputWidget.prototype.onDisable = function () {
15690         this.updateSearchIndicator();
15694  * Check if the input is {@link #readOnly read-only}.
15696  * @return {boolean}
15697  */
15698 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
15699         return this.readOnly;
15703  * Set the {@link #readOnly read-only} state of the input.
15705  * @param {boolean} state Make input read-only
15706  * @chainable
15707  */
15708 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
15709         this.readOnly = !!state;
15710         this.$input.prop( 'readOnly', this.readOnly );
15711         this.updateSearchIndicator();
15712         return this;
15716  * Support function for making #onElementAttach work across browsers.
15718  * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
15719  * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
15721  * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
15722  * first time that the element gets attached to the documented.
15723  */
15724 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
15725         var mutationObserver, onRemove, topmostNode, fakeParentNode,
15726                 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
15727                 widget = this;
15729         if ( MutationObserver ) {
15730                 // The new way. If only it wasn't so ugly.
15732                 if ( this.$element.closest( 'html' ).length ) {
15733                         // Widget is attached already, do nothing. This breaks the functionality of this function when
15734                         // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
15735                         // would require observation of the whole document, which would hurt performance of other,
15736                         // more important code.
15737                         return;
15738                 }
15740                 // Find topmost node in the tree
15741                 topmostNode = this.$element[ 0 ];
15742                 while ( topmostNode.parentNode ) {
15743                         topmostNode = topmostNode.parentNode;
15744                 }
15746                 // We have no way to detect the $element being attached somewhere without observing the entire
15747                 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
15748                 // parent node of $element, and instead detect when $element is removed from it (and thus
15749                 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
15750                 // doesn't get attached, we end up back here and create the parent.
15752                 mutationObserver = new MutationObserver( function ( mutations ) {
15753                         var i, j, removedNodes;
15754                         for ( i = 0; i < mutations.length; i++ ) {
15755                                 removedNodes = mutations[ i ].removedNodes;
15756                                 for ( j = 0; j < removedNodes.length; j++ ) {
15757                                         if ( removedNodes[ j ] === topmostNode ) {
15758                                                 setTimeout( onRemove, 0 );
15759                                                 return;
15760                                         }
15761                                 }
15762                         }
15763                 } );
15765                 onRemove = function () {
15766                         // If the node was attached somewhere else, report it
15767                         if ( widget.$element.closest( 'html' ).length ) {
15768                                 widget.onElementAttach();
15769                         }
15770                         mutationObserver.disconnect();
15771                         widget.installParentChangeDetector();
15772                 };
15774                 // Create a fake parent and observe it
15775                 fakeParentNode = $( '<div>' ).append( topmostNode )[ 0 ];
15776                 mutationObserver.observe( fakeParentNode, { childList: true } );
15777         } else {
15778                 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
15779                 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
15780                 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
15781         }
15785  * Automatically adjust the size of the text input.
15787  * This only affects #multiline inputs that are {@link #autosize autosized}.
15789  * @chainable
15790  */
15791 OO.ui.TextInputWidget.prototype.adjustSize = function () {
15792         var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
15794         if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
15795                 this.$clone
15796                         .val( this.$input.val() )
15797                         .attr( 'rows', this.minRows )
15798                         // Set inline height property to 0 to measure scroll height
15799                         .css( 'height', 0 );
15801                 this.$clone.removeClass( 'oo-ui-element-hidden' );
15803                 this.valCache = this.$input.val();
15805                 scrollHeight = this.$clone[ 0 ].scrollHeight;
15807                 // Remove inline height property to measure natural heights
15808                 this.$clone.css( 'height', '' );
15809                 innerHeight = this.$clone.innerHeight();
15810                 outerHeight = this.$clone.outerHeight();
15812                 // Measure max rows height
15813                 this.$clone
15814                         .attr( 'rows', this.maxRows )
15815                         .css( 'height', 'auto' )
15816                         .val( '' );
15817                 maxInnerHeight = this.$clone.innerHeight();
15819                 // Difference between reported innerHeight and scrollHeight with no scrollbars present
15820                 // Equals 1 on Blink-based browsers and 0 everywhere else
15821                 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
15822                 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
15824                 this.$clone.addClass( 'oo-ui-element-hidden' );
15826                 // Only apply inline height when expansion beyond natural height is needed
15827                 if ( idealHeight > innerHeight ) {
15828                         // Use the difference between the inner and outer height as a buffer
15829                         this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
15830                 } else {
15831                         this.$input.css( 'height', '' );
15832                 }
15833         }
15834         return this;
15838  * @inheritdoc
15839  * @protected
15840  */
15841 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
15842         return config.multiline ?
15843                 $( '<textarea>' ) :
15844                 $( '<input type="' + this.getSaneType( config ) + '" />' );
15848  * Get sanitized value for 'type' for given config.
15850  * @param {Object} config Configuration options
15851  * @return {string|null}
15852  * @private
15853  */
15854 OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
15855         var type = [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config.type ) !== -1 ?
15856                 config.type :
15857                 'text';
15858         return config.multiline ? 'multiline' : type;
15862  * Check if the input supports multiple lines.
15864  * @return {boolean}
15865  */
15866 OO.ui.TextInputWidget.prototype.isMultiline = function () {
15867         return !!this.multiline;
15871  * Check if the input automatically adjusts its size.
15873  * @return {boolean}
15874  */
15875 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
15876         return !!this.autosize;
15880  * Select the entire text of the input.
15882  * @chainable
15883  */
15884 OO.ui.TextInputWidget.prototype.select = function () {
15885         this.$input.select();
15886         return this;
15890  * Focus the input and move the cursor to the end.
15891  */
15892 OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
15893         var textRange,
15894                 element = this.$input[ 0 ];
15895         this.focus();
15896         if ( element.selectionStart !== undefined ) {
15897                 element.selectionStart = element.selectionEnd = element.value.length;
15898         } else if ( element.createTextRange ) {
15899                 // IE 8 and below
15900                 textRange = element.createTextRange();
15901                 textRange.collapse( false );
15902                 textRange.select();
15903         }
15907  * Set the validation pattern.
15909  * The validation pattern is either a regular expression, a function, or the symbolic name of a
15910  * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
15911  * value must contain only numbers).
15913  * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
15914  *  of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
15915  */
15916 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
15917         if ( validate instanceof RegExp || validate instanceof Function ) {
15918                 this.validate = validate;
15919         } else {
15920                 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
15921         }
15925  * Sets the 'invalid' flag appropriately.
15927  * @param {boolean} [isValid] Optionally override validation result
15928  */
15929 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
15930         var widget = this,
15931                 setFlag = function ( valid ) {
15932                         if ( !valid ) {
15933                                 widget.$input.attr( 'aria-invalid', 'true' );
15934                         } else {
15935                                 widget.$input.removeAttr( 'aria-invalid' );
15936                         }
15937                         widget.setFlags( { invalid: !valid } );
15938                 };
15940         if ( isValid !== undefined ) {
15941                 setFlag( isValid );
15942         } else {
15943                 this.getValidity().then( function () {
15944                         setFlag( true );
15945                 }, function () {
15946                         setFlag( false );
15947                 } );
15948         }
15952  * Check if a value is valid.
15954  * This method returns a promise that resolves with a boolean `true` if the current value is
15955  * considered valid according to the supplied {@link #validate validation pattern}.
15957  * @deprecated
15958  * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
15959  */
15960 OO.ui.TextInputWidget.prototype.isValid = function () {
15961         var result;
15963         if ( this.validate instanceof Function ) {
15964                 result = this.validate( this.getValue() );
15965                 if ( $.isFunction( result.promise ) ) {
15966                         return result.promise();
15967                 } else {
15968                         return $.Deferred().resolve( !!result ).promise();
15969                 }
15970         } else {
15971                 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
15972         }
15976  * Get the validity of current value.
15978  * This method returns a promise that resolves if the value is valid and rejects if
15979  * it isn't. Uses the {@link #validate validation pattern}  to check for validity.
15981  * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
15982  */
15983 OO.ui.TextInputWidget.prototype.getValidity = function () {
15984         var result, promise;
15986         function rejectOrResolve( valid ) {
15987                 if ( valid ) {
15988                         return $.Deferred().resolve().promise();
15989                 } else {
15990                         return $.Deferred().reject().promise();
15991                 }
15992         }
15994         if ( this.validate instanceof Function ) {
15995                 result = this.validate( this.getValue() );
15997                 if ( $.isFunction( result.promise ) ) {
15998                         promise = $.Deferred();
16000                         result.then( function ( valid ) {
16001                                 if ( valid ) {
16002                                         promise.resolve();
16003                                 } else {
16004                                         promise.reject();
16005                                 }
16006                         }, function () {
16007                                 promise.reject();
16008                         } );
16010                         return promise.promise();
16011                 } else {
16012                         return rejectOrResolve( result );
16013                 }
16014         } else {
16015                 return rejectOrResolve( this.getValue().match( this.validate ) );
16016         }
16020  * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
16022  * @param {string} labelPosition Label position, 'before' or 'after'
16023  * @chainable
16024  */
16025 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
16026         this.labelPosition = labelPosition;
16027         this.updatePosition();
16028         return this;
16032  * Deprecated alias of #setLabelPosition
16034  * @deprecated Use setLabelPosition instead.
16035  */
16036 OO.ui.TextInputWidget.prototype.setPosition =
16037         OO.ui.TextInputWidget.prototype.setLabelPosition;
16040  * Update the position of the inline label.
16042  * This method is called by #setLabelPosition, and can also be called on its own if
16043  * something causes the label to be mispositioned.
16045  * @chainable
16046  */
16047 OO.ui.TextInputWidget.prototype.updatePosition = function () {
16048         var after = this.labelPosition === 'after';
16050         this.$element
16051                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
16052                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
16054         this.positionLabel();
16056         return this;
16060  * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
16061  * already empty or when it's not editable.
16062  */
16063 OO.ui.TextInputWidget.prototype.updateSearchIndicator = function () {
16064         if ( this.type === 'search' ) {
16065                 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
16066                         this.setIndicator( null );
16067                 } else {
16068                         this.setIndicator( 'clear' );
16069                 }
16070         }
16074  * Position the label by setting the correct padding on the input.
16076  * @private
16077  * @chainable
16078  */
16079 OO.ui.TextInputWidget.prototype.positionLabel = function () {
16080         var after, rtl, property;
16081         // Clear old values
16082         this.$input
16083                 // Clear old values if present
16084                 .css( {
16085                         'padding-right': '',
16086                         'padding-left': ''
16087                 } );
16089         if ( this.label ) {
16090                 this.$element.append( this.$label );
16091         } else {
16092                 this.$label.detach();
16093                 return;
16094         }
16096         after = this.labelPosition === 'after';
16097         rtl = this.$element.css( 'direction' ) === 'rtl';
16098         property = after === rtl ? 'padding-left' : 'padding-right';
16100         this.$input.css( property, this.$label.outerWidth( true ) );
16102         return this;
16106  * @inheritdoc
16107  */
16108 OO.ui.TextInputWidget.prototype.gatherPreInfuseState = function ( node ) {
16109         var
16110                 state = OO.ui.TextInputWidget.parent.prototype.gatherPreInfuseState.call( this, node ),
16111                 $input = $( node ).find( '.oo-ui-inputWidget-input' );
16112         state.$input = $input; // shortcut for performance, used in InputWidget
16113         if ( this.multiline ) {
16114                 state.scrollTop = $input.scrollTop();
16115         }
16116         return state;
16120  * @inheritdoc
16121  */
16122 OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
16123         OO.ui.TextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
16124         if ( state.scrollTop !== undefined ) {
16125                 this.$input.scrollTop( state.scrollTop );
16126         }
16130  * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
16131  * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
16132  * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
16134  * - by typing a value in the text input field. If the value exactly matches the value of a menu
16135  *   option, that option will appear to be selected.
16136  * - by choosing a value from the menu. The value of the chosen option will then appear in the text
16137  *   input field.
16139  * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
16141  *     @example
16142  *     // Example: A ComboBoxWidget.
16143  *     var comboBox = new OO.ui.ComboBoxWidget( {
16144  *         label: 'ComboBoxWidget',
16145  *         input: { value: 'Option One' },
16146  *         menu: {
16147  *             items: [
16148  *                 new OO.ui.MenuOptionWidget( {
16149  *                     data: 'Option 1',
16150  *                     label: 'Option One'
16151  *                 } ),
16152  *                 new OO.ui.MenuOptionWidget( {
16153  *                     data: 'Option 2',
16154  *                     label: 'Option Two'
16155  *                 } ),
16156  *                 new OO.ui.MenuOptionWidget( {
16157  *                     data: 'Option 3',
16158  *                     label: 'Option Three'
16159  *                 } ),
16160  *                 new OO.ui.MenuOptionWidget( {
16161  *                     data: 'Option 4',
16162  *                     label: 'Option Four'
16163  *                 } ),
16164  *                 new OO.ui.MenuOptionWidget( {
16165  *                     data: 'Option 5',
16166  *                     label: 'Option Five'
16167  *                 } )
16168  *             ]
16169  *         }
16170  *     } );
16171  *     $( 'body' ).append( comboBox.$element );
16173  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
16175  * @class
16176  * @extends OO.ui.Widget
16177  * @mixins OO.ui.mixin.TabIndexedElement
16179  * @constructor
16180  * @param {Object} [config] Configuration options
16181  * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
16182  * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
16183  * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
16184  *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
16185  *  containing `<div>` and has a larger area. By default, the menu uses relative positioning.
16186  */
16187 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
16188         // Configuration initialization
16189         config = config || {};
16191         // Parent constructor
16192         OO.ui.ComboBoxWidget.parent.call( this, config );
16194         // Properties (must be set before TabIndexedElement constructor call)
16195         this.$indicator = this.$( '<span>' );
16197         // Mixin constructors
16198         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
16200         // Properties
16201         this.$overlay = config.$overlay || this.$element;
16202         this.input = new OO.ui.TextInputWidget( $.extend(
16203                 {
16204                         indicator: 'down',
16205                         $indicator: this.$indicator,
16206                         disabled: this.isDisabled()
16207                 },
16208                 config.input
16209         ) );
16210         this.input.$input.eq( 0 ).attr( {
16211                 role: 'combobox',
16212                 'aria-autocomplete': 'list'
16213         } );
16214         this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
16215                 {
16216                         widget: this,
16217                         input: this.input,
16218                         $container: this.input.$element,
16219                         disabled: this.isDisabled()
16220                 },
16221                 config.menu
16222         ) );
16224         // Events
16225         this.$indicator.on( {
16226                 click: this.onClick.bind( this ),
16227                 keypress: this.onKeyPress.bind( this )
16228         } );
16229         this.input.connect( this, {
16230                 change: 'onInputChange',
16231                 enter: 'onInputEnter'
16232         } );
16233         this.menu.connect( this, {
16234                 choose: 'onMenuChoose',
16235                 add: 'onMenuItemsChange',
16236                 remove: 'onMenuItemsChange'
16237         } );
16239         // Initialization
16240         this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
16241         this.$overlay.append( this.menu.$element );
16242         this.onMenuItemsChange();
16245 /* Setup */
16247 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
16248 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.mixin.TabIndexedElement );
16250 /* Methods */
16253  * Get the combobox's menu.
16254  * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
16255  */
16256 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
16257         return this.menu;
16261  * Get the combobox's text input widget.
16262  * @return {OO.ui.TextInputWidget} Text input widget
16263  */
16264 OO.ui.ComboBoxWidget.prototype.getInput = function () {
16265         return this.input;
16269  * Handle input change events.
16271  * @private
16272  * @param {string} value New value
16273  */
16274 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
16275         var match = this.menu.getItemFromData( value );
16277         this.menu.selectItem( match );
16278         if ( this.menu.getHighlightedItem() ) {
16279                 this.menu.highlightItem( match );
16280         }
16282         if ( !this.isDisabled() ) {
16283                 this.menu.toggle( true );
16284         }
16288  * Handle mouse click events.
16290  * @private
16291  * @param {jQuery.Event} e Mouse click event
16292  */
16293 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
16294         if ( !this.isDisabled() && e.which === 1 ) {
16295                 this.menu.toggle();
16296                 this.input.$input[ 0 ].focus();
16297         }
16298         return false;
16302  * Handle key press events.
16304  * @private
16305  * @param {jQuery.Event} e Key press event
16306  */
16307 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
16308         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
16309                 this.menu.toggle();
16310                 this.input.$input[ 0 ].focus();
16311                 return false;
16312         }
16316  * Handle input enter events.
16318  * @private
16319  */
16320 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
16321         if ( !this.isDisabled() ) {
16322                 this.menu.toggle( false );
16323         }
16327  * Handle menu choose events.
16329  * @private
16330  * @param {OO.ui.OptionWidget} item Chosen item
16331  */
16332 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
16333         this.input.setValue( item.getData() );
16337  * Handle menu item change events.
16339  * @private
16340  */
16341 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
16342         var match = this.menu.getItemFromData( this.input.getValue() );
16343         this.menu.selectItem( match );
16344         if ( this.menu.getHighlightedItem() ) {
16345                 this.menu.highlightItem( match );
16346         }
16347         this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
16351  * @inheritdoc
16352  */
16353 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
16354         // Parent method
16355         OO.ui.ComboBoxWidget.parent.prototype.setDisabled.call( this, disabled );
16357         if ( this.input ) {
16358                 this.input.setDisabled( this.isDisabled() );
16359         }
16360         if ( this.menu ) {
16361                 this.menu.setDisabled( this.isDisabled() );
16362         }
16364         return this;
16368  * LabelWidgets help identify the function of interface elements. Each LabelWidget can
16369  * be configured with a `label` option that is set to a string, a label node, or a function:
16371  * - String: a plaintext string
16372  * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
16373  *   label that includes a link or special styling, such as a gray color or additional graphical elements.
16374  * - Function: a function that will produce a string in the future. Functions are used
16375  *   in cases where the value of the label is not currently defined.
16377  * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
16378  * will come into focus when the label is clicked.
16380  *     @example
16381  *     // Examples of LabelWidgets
16382  *     var label1 = new OO.ui.LabelWidget( {
16383  *         label: 'plaintext label'
16384  *     } );
16385  *     var label2 = new OO.ui.LabelWidget( {
16386  *         label: $( '<a href="default.html">jQuery label</a>' )
16387  *     } );
16388  *     // Create a fieldset layout with fields for each example
16389  *     var fieldset = new OO.ui.FieldsetLayout();
16390  *     fieldset.addItems( [
16391  *         new OO.ui.FieldLayout( label1 ),
16392  *         new OO.ui.FieldLayout( label2 )
16393  *     ] );
16394  *     $( 'body' ).append( fieldset.$element );
16396  * @class
16397  * @extends OO.ui.Widget
16398  * @mixins OO.ui.mixin.LabelElement
16400  * @constructor
16401  * @param {Object} [config] Configuration options
16402  * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
16403  *  Clicking the label will focus the specified input field.
16404  */
16405 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
16406         // Configuration initialization
16407         config = config || {};
16409         // Parent constructor
16410         OO.ui.LabelWidget.parent.call( this, config );
16412         // Mixin constructors
16413         OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
16414         OO.ui.mixin.TitledElement.call( this, config );
16416         // Properties
16417         this.input = config.input;
16419         // Events
16420         if ( this.input instanceof OO.ui.InputWidget ) {
16421                 this.$element.on( 'click', this.onClick.bind( this ) );
16422         }
16424         // Initialization
16425         this.$element.addClass( 'oo-ui-labelWidget' );
16428 /* Setup */
16430 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
16431 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
16432 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
16434 /* Static Properties */
16436 OO.ui.LabelWidget.static.tagName = 'span';
16438 /* Methods */
16441  * Handles label mouse click events.
16443  * @private
16444  * @param {jQuery.Event} e Mouse click event
16445  */
16446 OO.ui.LabelWidget.prototype.onClick = function () {
16447         this.input.simulateLabelClick();
16448         return false;
16452  * OptionWidgets are special elements that can be selected and configured with data. The
16453  * data is often unique for each option, but it does not have to be. OptionWidgets are used
16454  * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
16455  * and examples, please see the [OOjs UI documentation on MediaWiki][1].
16457  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16459  * @class
16460  * @extends OO.ui.Widget
16461  * @mixins OO.ui.mixin.LabelElement
16462  * @mixins OO.ui.mixin.FlaggedElement
16464  * @constructor
16465  * @param {Object} [config] Configuration options
16466  */
16467 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
16468         // Configuration initialization
16469         config = config || {};
16471         // Parent constructor
16472         OO.ui.OptionWidget.parent.call( this, config );
16474         // Mixin constructors
16475         OO.ui.mixin.ItemWidget.call( this );
16476         OO.ui.mixin.LabelElement.call( this, config );
16477         OO.ui.mixin.FlaggedElement.call( this, config );
16479         // Properties
16480         this.selected = false;
16481         this.highlighted = false;
16482         this.pressed = false;
16484         // Initialization
16485         this.$element
16486                 .data( 'oo-ui-optionWidget', this )
16487                 .attr( 'role', 'option' )
16488                 .attr( 'aria-selected', 'false' )
16489                 .addClass( 'oo-ui-optionWidget' )
16490                 .append( this.$label );
16493 /* Setup */
16495 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
16496 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
16497 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
16498 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
16500 /* Static Properties */
16502 OO.ui.OptionWidget.static.selectable = true;
16504 OO.ui.OptionWidget.static.highlightable = true;
16506 OO.ui.OptionWidget.static.pressable = true;
16508 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
16510 /* Methods */
16513  * Check if the option can be selected.
16515  * @return {boolean} Item is selectable
16516  */
16517 OO.ui.OptionWidget.prototype.isSelectable = function () {
16518         return this.constructor.static.selectable && !this.isDisabled() && this.isVisible();
16522  * Check if the option can be highlighted. A highlight indicates that the option
16523  * may be selected when a user presses enter or clicks. Disabled items cannot
16524  * be highlighted.
16526  * @return {boolean} Item is highlightable
16527  */
16528 OO.ui.OptionWidget.prototype.isHighlightable = function () {
16529         return this.constructor.static.highlightable && !this.isDisabled() && this.isVisible();
16533  * Check if the option can be pressed. The pressed state occurs when a user mouses
16534  * down on an item, but has not yet let go of the mouse.
16536  * @return {boolean} Item is pressable
16537  */
16538 OO.ui.OptionWidget.prototype.isPressable = function () {
16539         return this.constructor.static.pressable && !this.isDisabled() && this.isVisible();
16543  * Check if the option is selected.
16545  * @return {boolean} Item is selected
16546  */
16547 OO.ui.OptionWidget.prototype.isSelected = function () {
16548         return this.selected;
16552  * Check if the option is highlighted. A highlight indicates that the
16553  * item may be selected when a user presses enter or clicks.
16555  * @return {boolean} Item is highlighted
16556  */
16557 OO.ui.OptionWidget.prototype.isHighlighted = function () {
16558         return this.highlighted;
16562  * Check if the option is pressed. The pressed state occurs when a user mouses
16563  * down on an item, but has not yet let go of the mouse. The item may appear
16564  * selected, but it will not be selected until the user releases the mouse.
16566  * @return {boolean} Item is pressed
16567  */
16568 OO.ui.OptionWidget.prototype.isPressed = function () {
16569         return this.pressed;
16573  * Set the option’s selected state. In general, all modifications to the selection
16574  * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
16575  * method instead of this method.
16577  * @param {boolean} [state=false] Select option
16578  * @chainable
16579  */
16580 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
16581         if ( this.constructor.static.selectable ) {
16582                 this.selected = !!state;
16583                 this.$element
16584                         .toggleClass( 'oo-ui-optionWidget-selected', state )
16585                         .attr( 'aria-selected', state.toString() );
16586                 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
16587                         this.scrollElementIntoView();
16588                 }
16589                 this.updateThemeClasses();
16590         }
16591         return this;
16595  * Set the option’s highlighted state. In general, all programmatic
16596  * modifications to the highlight should be handled by the
16597  * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
16598  * method instead of this method.
16600  * @param {boolean} [state=false] Highlight option
16601  * @chainable
16602  */
16603 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
16604         if ( this.constructor.static.highlightable ) {
16605                 this.highlighted = !!state;
16606                 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
16607                 this.updateThemeClasses();
16608         }
16609         return this;
16613  * Set the option’s pressed state. In general, all
16614  * programmatic modifications to the pressed state should be handled by the
16615  * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
16616  * method instead of this method.
16618  * @param {boolean} [state=false] Press option
16619  * @chainable
16620  */
16621 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
16622         if ( this.constructor.static.pressable ) {
16623                 this.pressed = !!state;
16624                 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
16625                 this.updateThemeClasses();
16626         }
16627         return this;
16631  * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
16632  * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
16633  * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
16634  * options. For more information about options and selects, please see the
16635  * [OOjs UI documentation on MediaWiki][1].
16637  *     @example
16638  *     // Decorated options in a select widget
16639  *     var select = new OO.ui.SelectWidget( {
16640  *         items: [
16641  *             new OO.ui.DecoratedOptionWidget( {
16642  *                 data: 'a',
16643  *                 label: 'Option with icon',
16644  *                 icon: 'help'
16645  *             } ),
16646  *             new OO.ui.DecoratedOptionWidget( {
16647  *                 data: 'b',
16648  *                 label: 'Option with indicator',
16649  *                 indicator: 'next'
16650  *             } )
16651  *         ]
16652  *     } );
16653  *     $( 'body' ).append( select.$element );
16655  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16657  * @class
16658  * @extends OO.ui.OptionWidget
16659  * @mixins OO.ui.mixin.IconElement
16660  * @mixins OO.ui.mixin.IndicatorElement
16662  * @constructor
16663  * @param {Object} [config] Configuration options
16664  */
16665 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
16666         // Parent constructor
16667         OO.ui.DecoratedOptionWidget.parent.call( this, config );
16669         // Mixin constructors
16670         OO.ui.mixin.IconElement.call( this, config );
16671         OO.ui.mixin.IndicatorElement.call( this, config );
16673         // Initialization
16674         this.$element
16675                 .addClass( 'oo-ui-decoratedOptionWidget' )
16676                 .prepend( this.$icon )
16677                 .append( this.$indicator );
16680 /* Setup */
16682 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
16683 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
16684 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
16687  * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
16688  * can be selected and configured with data. The class is
16689  * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
16690  * [OOjs UI documentation on MediaWiki] [1] for more information.
16692  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
16694  * @class
16695  * @extends OO.ui.DecoratedOptionWidget
16696  * @mixins OO.ui.mixin.ButtonElement
16697  * @mixins OO.ui.mixin.TabIndexedElement
16698  * @mixins OO.ui.mixin.TitledElement
16700  * @constructor
16701  * @param {Object} [config] Configuration options
16702  */
16703 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
16704         // Configuration initialization
16705         config = config || {};
16707         // Parent constructor
16708         OO.ui.ButtonOptionWidget.parent.call( this, config );
16710         // Mixin constructors
16711         OO.ui.mixin.ButtonElement.call( this, config );
16712         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
16713         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, {
16714                 $tabIndexed: this.$button,
16715                 tabIndex: -1
16716         } ) );
16718         // Initialization
16719         this.$element.addClass( 'oo-ui-buttonOptionWidget' );
16720         this.$button.append( this.$element.contents() );
16721         this.$element.append( this.$button );
16724 /* Setup */
16726 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
16727 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
16728 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TitledElement );
16729 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TabIndexedElement );
16731 /* Static Properties */
16733 // Allow button mouse down events to pass through so they can be handled by the parent select widget
16734 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
16736 OO.ui.ButtonOptionWidget.static.highlightable = false;
16738 /* Methods */
16741  * @inheritdoc
16742  */
16743 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
16744         OO.ui.ButtonOptionWidget.parent.prototype.setSelected.call( this, state );
16746         if ( this.constructor.static.selectable ) {
16747                 this.setActive( state );
16748         }
16750         return this;
16754  * RadioOptionWidget is an option widget that looks like a radio button.
16755  * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
16756  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
16758  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
16760  * @class
16761  * @extends OO.ui.OptionWidget
16763  * @constructor
16764  * @param {Object} [config] Configuration options
16765  */
16766 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
16767         // Configuration initialization
16768         config = config || {};
16770         // Properties (must be done before parent constructor which calls #setDisabled)
16771         this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
16773         // Parent constructor
16774         OO.ui.RadioOptionWidget.parent.call( this, config );
16776         // Events
16777         this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
16779         // Initialization
16780         // Remove implicit role, we're handling it ourselves
16781         this.radio.$input.attr( 'role', 'presentation' );
16782         this.$element
16783                 .addClass( 'oo-ui-radioOptionWidget' )
16784                 .attr( 'role', 'radio' )
16785                 .attr( 'aria-checked', 'false' )
16786                 .removeAttr( 'aria-selected' )
16787                 .prepend( this.radio.$element );
16790 /* Setup */
16792 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
16794 /* Static Properties */
16796 OO.ui.RadioOptionWidget.static.highlightable = false;
16798 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
16800 OO.ui.RadioOptionWidget.static.pressable = false;
16802 OO.ui.RadioOptionWidget.static.tagName = 'label';
16804 /* Methods */
16807  * @param {jQuery.Event} e Focus event
16808  * @private
16809  */
16810 OO.ui.RadioOptionWidget.prototype.onInputFocus = function () {
16811         this.radio.$input.blur();
16812         this.$element.parent().focus();
16816  * @inheritdoc
16817  */
16818 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
16819         OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
16821         this.radio.setSelected( state );
16822         this.$element
16823                 .attr( 'aria-checked', state.toString() )
16824                 .removeAttr( 'aria-selected' );
16826         return this;
16830  * @inheritdoc
16831  */
16832 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
16833         OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
16835         this.radio.setDisabled( this.isDisabled() );
16837         return this;
16841  * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
16842  * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
16843  * the [OOjs UI documentation on MediaWiki] [1] for more information.
16845  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
16847  * @class
16848  * @extends OO.ui.DecoratedOptionWidget
16850  * @constructor
16851  * @param {Object} [config] Configuration options
16852  */
16853 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
16854         // Configuration initialization
16855         config = $.extend( { icon: 'check' }, config );
16857         // Parent constructor
16858         OO.ui.MenuOptionWidget.parent.call( this, config );
16860         // Initialization
16861         this.$element
16862                 .attr( 'role', 'menuitem' )
16863                 .addClass( 'oo-ui-menuOptionWidget' );
16866 /* Setup */
16868 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
16870 /* Static Properties */
16872 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
16875  * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
16876  * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
16878  *     @example
16879  *     var myDropdown = new OO.ui.DropdownWidget( {
16880  *         menu: {
16881  *             items: [
16882  *                 new OO.ui.MenuSectionOptionWidget( {
16883  *                     label: 'Dogs'
16884  *                 } ),
16885  *                 new OO.ui.MenuOptionWidget( {
16886  *                     data: 'corgi',
16887  *                     label: 'Welsh Corgi'
16888  *                 } ),
16889  *                 new OO.ui.MenuOptionWidget( {
16890  *                     data: 'poodle',
16891  *                     label: 'Standard Poodle'
16892  *                 } ),
16893  *                 new OO.ui.MenuSectionOptionWidget( {
16894  *                     label: 'Cats'
16895  *                 } ),
16896  *                 new OO.ui.MenuOptionWidget( {
16897  *                     data: 'lion',
16898  *                     label: 'Lion'
16899  *                 } )
16900  *             ]
16901  *         }
16902  *     } );
16903  *     $( 'body' ).append( myDropdown.$element );
16905  * @class
16906  * @extends OO.ui.DecoratedOptionWidget
16908  * @constructor
16909  * @param {Object} [config] Configuration options
16910  */
16911 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
16912         // Parent constructor
16913         OO.ui.MenuSectionOptionWidget.parent.call( this, config );
16915         // Initialization
16916         this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
16919 /* Setup */
16921 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
16923 /* Static Properties */
16925 OO.ui.MenuSectionOptionWidget.static.selectable = false;
16927 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
16930  * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
16932  * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
16933  * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
16934  * for an example.
16936  * @class
16937  * @extends OO.ui.DecoratedOptionWidget
16939  * @constructor
16940  * @param {Object} [config] Configuration options
16941  * @cfg {number} [level] Indentation level
16942  * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
16943  */
16944 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
16945         // Configuration initialization
16946         config = config || {};
16948         // Parent constructor
16949         OO.ui.OutlineOptionWidget.parent.call( this, config );
16951         // Properties
16952         this.level = 0;
16953         this.movable = !!config.movable;
16954         this.removable = !!config.removable;
16956         // Initialization
16957         this.$element.addClass( 'oo-ui-outlineOptionWidget' );
16958         this.setLevel( config.level );
16961 /* Setup */
16963 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
16965 /* Static Properties */
16967 OO.ui.OutlineOptionWidget.static.highlightable = false;
16969 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
16971 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
16973 OO.ui.OutlineOptionWidget.static.levels = 3;
16975 /* Methods */
16978  * Check if item is movable.
16980  * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
16982  * @return {boolean} Item is movable
16983  */
16984 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
16985         return this.movable;
16989  * Check if item is removable.
16991  * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
16993  * @return {boolean} Item is removable
16994  */
16995 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
16996         return this.removable;
17000  * Get indentation level.
17002  * @return {number} Indentation level
17003  */
17004 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
17005         return this.level;
17009  * Set movability.
17011  * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17013  * @param {boolean} movable Item is movable
17014  * @chainable
17015  */
17016 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
17017         this.movable = !!movable;
17018         this.updateThemeClasses();
17019         return this;
17023  * Set removability.
17025  * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17027  * @param {boolean} movable Item is removable
17028  * @chainable
17029  */
17030 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
17031         this.removable = !!removable;
17032         this.updateThemeClasses();
17033         return this;
17037  * Set indentation level.
17039  * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
17040  * @chainable
17041  */
17042 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
17043         var levels = this.constructor.static.levels,
17044                 levelClass = this.constructor.static.levelClass,
17045                 i = levels;
17047         this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
17048         while ( i-- ) {
17049                 if ( this.level === i ) {
17050                         this.$element.addClass( levelClass + i );
17051                 } else {
17052                         this.$element.removeClass( levelClass + i );
17053                 }
17054         }
17055         this.updateThemeClasses();
17057         return this;
17061  * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
17063  * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
17064  * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
17065  * for an example.
17067  * @class
17068  * @extends OO.ui.OptionWidget
17070  * @constructor
17071  * @param {Object} [config] Configuration options
17072  */
17073 OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
17074         // Configuration initialization
17075         config = config || {};
17077         // Parent constructor
17078         OO.ui.TabOptionWidget.parent.call( this, config );
17080         // Initialization
17081         this.$element.addClass( 'oo-ui-tabOptionWidget' );
17084 /* Setup */
17086 OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
17088 /* Static Properties */
17090 OO.ui.TabOptionWidget.static.highlightable = false;
17093  * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
17094  * By default, each popup has an anchor that points toward its origin.
17095  * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
17097  *     @example
17098  *     // A popup widget.
17099  *     var popup = new OO.ui.PopupWidget( {
17100  *         $content: $( '<p>Hi there!</p>' ),
17101  *         padded: true,
17102  *         width: 300
17103  *     } );
17105  *     $( 'body' ).append( popup.$element );
17106  *     // To display the popup, toggle the visibility to 'true'.
17107  *     popup.toggle( true );
17109  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
17111  * @class
17112  * @extends OO.ui.Widget
17113  * @mixins OO.ui.mixin.LabelElement
17114  * @mixins OO.ui.mixin.ClippableElement
17116  * @constructor
17117  * @param {Object} [config] Configuration options
17118  * @cfg {number} [width=320] Width of popup in pixels
17119  * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
17120  * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
17121  * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
17122  *  If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
17123  *  popup is leaning towards the right of the screen.
17124  *  Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
17125  *  in the given language, which means it will flip to the correct positioning in right-to-left languages.
17126  *  Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
17127  *  sentence in the given language.
17128  * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
17129  *  See the [OOjs UI docs on MediaWiki][3] for an example.
17130  *  [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
17131  * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
17132  * @cfg {jQuery} [$content] Content to append to the popup's body
17133  * @cfg {jQuery} [$footer] Content to append to the popup's footer
17134  * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
17135  * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
17136  *  This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
17137  *  for an example.
17138  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
17139  * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
17140  *  button.
17141  * @cfg {boolean} [padded] Add padding to the popup's body
17142  */
17143 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
17144         // Configuration initialization
17145         config = config || {};
17147         // Parent constructor
17148         OO.ui.PopupWidget.parent.call( this, config );
17150         // Properties (must be set before ClippableElement constructor call)
17151         this.$body = $( '<div>' );
17152         this.$popup = $( '<div>' );
17154         // Mixin constructors
17155         OO.ui.mixin.LabelElement.call( this, config );
17156         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, {
17157                 $clippable: this.$body,
17158                 $clippableContainer: this.$popup
17159         } ) );
17161         // Properties
17162         this.$head = $( '<div>' );
17163         this.$footer = $( '<div>' );
17164         this.$anchor = $( '<div>' );
17165         // If undefined, will be computed lazily in updateDimensions()
17166         this.$container = config.$container;
17167         this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
17168         this.autoClose = !!config.autoClose;
17169         this.$autoCloseIgnore = config.$autoCloseIgnore;
17170         this.transitionTimeout = null;
17171         this.anchor = null;
17172         this.width = config.width !== undefined ? config.width : 320;
17173         this.height = config.height !== undefined ? config.height : null;
17174         this.setAlignment( config.align );
17175         this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
17176         this.onMouseDownHandler = this.onMouseDown.bind( this );
17177         this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
17179         // Events
17180         this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
17182         // Initialization
17183         this.toggleAnchor( config.anchor === undefined || config.anchor );
17184         this.$body.addClass( 'oo-ui-popupWidget-body' );
17185         this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
17186         this.$head
17187                 .addClass( 'oo-ui-popupWidget-head' )
17188                 .append( this.$label, this.closeButton.$element );
17189         this.$footer.addClass( 'oo-ui-popupWidget-footer' );
17190         if ( !config.head ) {
17191                 this.$head.addClass( 'oo-ui-element-hidden' );
17192         }
17193         if ( !config.$footer ) {
17194                 this.$footer.addClass( 'oo-ui-element-hidden' );
17195         }
17196         this.$popup
17197                 .addClass( 'oo-ui-popupWidget-popup' )
17198                 .append( this.$head, this.$body, this.$footer );
17199         this.$element
17200                 .addClass( 'oo-ui-popupWidget' )
17201                 .append( this.$popup, this.$anchor );
17202         // Move content, which was added to #$element by OO.ui.Widget, to the body
17203         if ( config.$content instanceof jQuery ) {
17204                 this.$body.append( config.$content );
17205         }
17206         if ( config.$footer instanceof jQuery ) {
17207                 this.$footer.append( config.$footer );
17208         }
17209         if ( config.padded ) {
17210                 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
17211         }
17213         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
17214         // that reference properties not initialized at that time of parent class construction
17215         // TODO: Find a better way to handle post-constructor setup
17216         this.visible = false;
17217         this.$element.addClass( 'oo-ui-element-hidden' );
17220 /* Setup */
17222 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
17223 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
17224 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
17226 /* Methods */
17229  * Handles mouse down events.
17231  * @private
17232  * @param {MouseEvent} e Mouse down event
17233  */
17234 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
17235         if (
17236                 this.isVisible() &&
17237                 !$.contains( this.$element[ 0 ], e.target ) &&
17238                 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
17239         ) {
17240                 this.toggle( false );
17241         }
17245  * Bind mouse down listener.
17247  * @private
17248  */
17249 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
17250         // Capture clicks outside popup
17251         OO.ui.addCaptureEventListener( this.getElementWindow(), 'mousedown', this.onMouseDownHandler );
17255  * Handles close button click events.
17257  * @private
17258  */
17259 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
17260         if ( this.isVisible() ) {
17261                 this.toggle( false );
17262         }
17266  * Unbind mouse down listener.
17268  * @private
17269  */
17270 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
17271         OO.ui.removeCaptureEventListener( this.getElementWindow(), 'mousedown', this.onMouseDownHandler );
17275  * Handles key down events.
17277  * @private
17278  * @param {KeyboardEvent} e Key down event
17279  */
17280 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
17281         if (
17282                 e.which === OO.ui.Keys.ESCAPE &&
17283                 this.isVisible()
17284         ) {
17285                 this.toggle( false );
17286                 e.preventDefault();
17287                 e.stopPropagation();
17288         }
17292  * Bind key down listener.
17294  * @private
17295  */
17296 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
17297         OO.ui.addCaptureEventListener( this.getElementWindow(), 'keydown', this.onDocumentKeyDownHandler );
17301  * Unbind key down listener.
17303  * @private
17304  */
17305 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
17306         OO.ui.removeCaptureEventListener( this.getElementWindow(), 'keydown', this.onDocumentKeyDownHandler );
17310  * Show, hide, or toggle the visibility of the anchor.
17312  * @param {boolean} [show] Show anchor, omit to toggle
17313  */
17314 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
17315         show = show === undefined ? !this.anchored : !!show;
17317         if ( this.anchored !== show ) {
17318                 if ( show ) {
17319                         this.$element.addClass( 'oo-ui-popupWidget-anchored' );
17320                 } else {
17321                         this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
17322                 }
17323                 this.anchored = show;
17324         }
17328  * Check if the anchor is visible.
17330  * @return {boolean} Anchor is visible
17331  */
17332 OO.ui.PopupWidget.prototype.hasAnchor = function () {
17333         return this.anchor;
17337  * @inheritdoc
17338  */
17339 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
17340         var change;
17341         show = show === undefined ? !this.isVisible() : !!show;
17343         change = show !== this.isVisible();
17345         // Parent method
17346         OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
17348         if ( change ) {
17349                 if ( show ) {
17350                         if ( this.autoClose ) {
17351                                 this.bindMouseDownListener();
17352                                 this.bindKeyDownListener();
17353                         }
17354                         this.updateDimensions();
17355                         this.toggleClipping( true );
17356                 } else {
17357                         this.toggleClipping( false );
17358                         if ( this.autoClose ) {
17359                                 this.unbindMouseDownListener();
17360                                 this.unbindKeyDownListener();
17361                         }
17362                 }
17363         }
17365         return this;
17369  * Set the size of the popup.
17371  * Changing the size may also change the popup's position depending on the alignment.
17373  * @param {number} width Width in pixels
17374  * @param {number} height Height in pixels
17375  * @param {boolean} [transition=false] Use a smooth transition
17376  * @chainable
17377  */
17378 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
17379         this.width = width;
17380         this.height = height !== undefined ? height : null;
17381         if ( this.isVisible() ) {
17382                 this.updateDimensions( transition );
17383         }
17387  * Update the size and position.
17389  * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
17390  * be called automatically.
17392  * @param {boolean} [transition=false] Use a smooth transition
17393  * @chainable
17394  */
17395 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
17396         var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
17397                 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
17398                 align = this.align,
17399                 widget = this;
17401         if ( !this.$container ) {
17402                 // Lazy-initialize $container if not specified in constructor
17403                 this.$container = $( this.getClosestScrollableElementContainer() );
17404         }
17406         // Set height and width before measuring things, since it might cause our measurements
17407         // to change (e.g. due to scrollbars appearing or disappearing)
17408         this.$popup.css( {
17409                 width: this.width,
17410                 height: this.height !== null ? this.height : 'auto'
17411         } );
17413         // If we are in RTL, we need to flip the alignment, unless it is center
17414         if ( align === 'forwards' || align === 'backwards' ) {
17415                 if ( this.$container.css( 'direction' ) === 'rtl' ) {
17416                         align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
17417                 } else {
17418                         align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
17419                 }
17421         }
17423         // Compute initial popupOffset based on alignment
17424         popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
17426         // Figure out if this will cause the popup to go beyond the edge of the container
17427         originOffset = this.$element.offset().left;
17428         containerLeft = this.$container.offset().left;
17429         containerWidth = this.$container.innerWidth();
17430         containerRight = containerLeft + containerWidth;
17431         popupLeft = popupOffset - this.containerPadding;
17432         popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
17433         overlapLeft = ( originOffset + popupLeft ) - containerLeft;
17434         overlapRight = containerRight - ( originOffset + popupRight );
17436         // Adjust offset to make the popup not go beyond the edge, if needed
17437         if ( overlapRight < 0 ) {
17438                 popupOffset += overlapRight;
17439         } else if ( overlapLeft < 0 ) {
17440                 popupOffset -= overlapLeft;
17441         }
17443         // Adjust offset to avoid anchor being rendered too close to the edge
17444         // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
17445         // TODO: Find a measurement that works for CSS anchors and image anchors
17446         anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
17447         if ( popupOffset + this.width < anchorWidth ) {
17448                 popupOffset = anchorWidth - this.width;
17449         } else if ( -popupOffset < anchorWidth ) {
17450                 popupOffset = -anchorWidth;
17451         }
17453         // Prevent transition from being interrupted
17454         clearTimeout( this.transitionTimeout );
17455         if ( transition ) {
17456                 // Enable transition
17457                 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
17458         }
17460         // Position body relative to anchor
17461         this.$popup.css( 'margin-left', popupOffset );
17463         if ( transition ) {
17464                 // Prevent transitioning after transition is complete
17465                 this.transitionTimeout = setTimeout( function () {
17466                         widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
17467                 }, 200 );
17468         } else {
17469                 // Prevent transitioning immediately
17470                 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
17471         }
17473         // Reevaluate clipping state since we've relocated and resized the popup
17474         this.clip();
17476         return this;
17480  * Set popup alignment
17481  * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
17482  *  `backwards` or `forwards`.
17483  */
17484 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
17485         // Validate alignment and transform deprecated values
17486         if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
17487                 this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
17488         } else {
17489                 this.align = 'center';
17490         }
17494  * Get popup alignment
17495  * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
17496  *  `backwards` or `forwards`.
17497  */
17498 OO.ui.PopupWidget.prototype.getAlignment = function () {
17499         return this.align;
17503  * Progress bars visually display the status of an operation, such as a download,
17504  * and can be either determinate or indeterminate:
17506  * - **determinate** process bars show the percent of an operation that is complete.
17508  * - **indeterminate** process bars use a visual display of motion to indicate that an operation
17509  *   is taking place. Because the extent of an indeterminate operation is unknown, the bar does
17510  *   not use percentages.
17512  * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
17514  *     @example
17515  *     // Examples of determinate and indeterminate progress bars.
17516  *     var progressBar1 = new OO.ui.ProgressBarWidget( {
17517  *         progress: 33
17518  *     } );
17519  *     var progressBar2 = new OO.ui.ProgressBarWidget();
17521  *     // Create a FieldsetLayout to layout progress bars
17522  *     var fieldset = new OO.ui.FieldsetLayout;
17523  *     fieldset.addItems( [
17524  *        new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
17525  *        new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
17526  *     ] );
17527  *     $( 'body' ).append( fieldset.$element );
17529  * @class
17530  * @extends OO.ui.Widget
17532  * @constructor
17533  * @param {Object} [config] Configuration options
17534  * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
17535  *  To create a determinate progress bar, specify a number that reflects the initial percent complete.
17536  *  By default, the progress bar is indeterminate.
17537  */
17538 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
17539         // Configuration initialization
17540         config = config || {};
17542         // Parent constructor
17543         OO.ui.ProgressBarWidget.parent.call( this, config );
17545         // Properties
17546         this.$bar = $( '<div>' );
17547         this.progress = null;
17549         // Initialization
17550         this.setProgress( config.progress !== undefined ? config.progress : false );
17551         this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
17552         this.$element
17553                 .attr( {
17554                         role: 'progressbar',
17555                         'aria-valuemin': 0,
17556                         'aria-valuemax': 100
17557                 } )
17558                 .addClass( 'oo-ui-progressBarWidget' )
17559                 .append( this.$bar );
17562 /* Setup */
17564 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
17566 /* Static Properties */
17568 OO.ui.ProgressBarWidget.static.tagName = 'div';
17570 /* Methods */
17573  * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
17575  * @return {number|boolean} Progress percent
17576  */
17577 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
17578         return this.progress;
17582  * Set the percent of the process completed or `false` for an indeterminate process.
17584  * @param {number|boolean} progress Progress percent or `false` for indeterminate
17585  */
17586 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
17587         this.progress = progress;
17589         if ( progress !== false ) {
17590                 this.$bar.css( 'width', this.progress + '%' );
17591                 this.$element.attr( 'aria-valuenow', this.progress );
17592         } else {
17593                 this.$bar.css( 'width', '' );
17594                 this.$element.removeAttr( 'aria-valuenow' );
17595         }
17596         this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
17600  * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
17601  * and a menu of search results, which is displayed beneath the query
17602  * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
17603  * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
17604  * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
17606  * Each time the query is changed, the search result menu is cleared and repopulated. Please see
17607  * the [OOjs UI demos][1] for an example.
17609  * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
17611  * @class
17612  * @extends OO.ui.Widget
17614  * @constructor
17615  * @param {Object} [config] Configuration options
17616  * @cfg {string|jQuery} [placeholder] Placeholder text for query input
17617  * @cfg {string} [value] Initial query value
17618  */
17619 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
17620         // Configuration initialization
17621         config = config || {};
17623         // Parent constructor
17624         OO.ui.SearchWidget.parent.call( this, config );
17626         // Properties
17627         this.query = new OO.ui.TextInputWidget( {
17628                 icon: 'search',
17629                 placeholder: config.placeholder,
17630                 value: config.value
17631         } );
17632         this.results = new OO.ui.SelectWidget();
17633         this.$query = $( '<div>' );
17634         this.$results = $( '<div>' );
17636         // Events
17637         this.query.connect( this, {
17638                 change: 'onQueryChange',
17639                 enter: 'onQueryEnter'
17640         } );
17641         this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
17643         // Initialization
17644         this.$query
17645                 .addClass( 'oo-ui-searchWidget-query' )
17646                 .append( this.query.$element );
17647         this.$results
17648                 .addClass( 'oo-ui-searchWidget-results' )
17649                 .append( this.results.$element );
17650         this.$element
17651                 .addClass( 'oo-ui-searchWidget' )
17652                 .append( this.$results, this.$query );
17655 /* Setup */
17657 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
17659 /* Methods */
17662  * Handle query key down events.
17664  * @private
17665  * @param {jQuery.Event} e Key down event
17666  */
17667 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
17668         var highlightedItem, nextItem,
17669                 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
17671         if ( dir ) {
17672                 highlightedItem = this.results.getHighlightedItem();
17673                 if ( !highlightedItem ) {
17674                         highlightedItem = this.results.getSelectedItem();
17675                 }
17676                 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
17677                 this.results.highlightItem( nextItem );
17678                 nextItem.scrollElementIntoView();
17679         }
17683  * Handle select widget select events.
17685  * Clears existing results. Subclasses should repopulate items according to new query.
17687  * @private
17688  * @param {string} value New value
17689  */
17690 OO.ui.SearchWidget.prototype.onQueryChange = function () {
17691         // Reset
17692         this.results.clearItems();
17696  * Handle select widget enter key events.
17698  * Chooses highlighted item.
17700  * @private
17701  * @param {string} value New value
17702  */
17703 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
17704         var highlightedItem = this.results.getHighlightedItem();
17705         if ( highlightedItem ) {
17706                 this.results.chooseItem( highlightedItem );
17707         }
17711  * Get the query input.
17713  * @return {OO.ui.TextInputWidget} Query input
17714  */
17715 OO.ui.SearchWidget.prototype.getQuery = function () {
17716         return this.query;
17720  * Get the search results menu.
17722  * @return {OO.ui.SelectWidget} Menu of search results
17723  */
17724 OO.ui.SearchWidget.prototype.getResults = function () {
17725         return this.results;
17729  * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
17730  * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
17731  * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
17732  * menu selects}.
17734  * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
17735  * information, please see the [OOjs UI documentation on MediaWiki][1].
17737  *     @example
17738  *     // Example of a select widget with three options
17739  *     var select = new OO.ui.SelectWidget( {
17740  *         items: [
17741  *             new OO.ui.OptionWidget( {
17742  *                 data: 'a',
17743  *                 label: 'Option One',
17744  *             } ),
17745  *             new OO.ui.OptionWidget( {
17746  *                 data: 'b',
17747  *                 label: 'Option Two',
17748  *             } ),
17749  *             new OO.ui.OptionWidget( {
17750  *                 data: 'c',
17751  *                 label: 'Option Three',
17752  *             } )
17753  *         ]
17754  *     } );
17755  *     $( 'body' ).append( select.$element );
17757  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
17759  * @abstract
17760  * @class
17761  * @extends OO.ui.Widget
17762  * @mixins OO.ui.mixin.GroupWidget
17764  * @constructor
17765  * @param {Object} [config] Configuration options
17766  * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
17767  *  Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
17768  *  the [OOjs UI documentation on MediaWiki] [2] for examples.
17769  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
17770  */
17771 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
17772         // Configuration initialization
17773         config = config || {};
17775         // Parent constructor
17776         OO.ui.SelectWidget.parent.call( this, config );
17778         // Mixin constructors
17779         OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
17781         // Properties
17782         this.pressed = false;
17783         this.selecting = null;
17784         this.onMouseUpHandler = this.onMouseUp.bind( this );
17785         this.onMouseMoveHandler = this.onMouseMove.bind( this );
17786         this.onKeyDownHandler = this.onKeyDown.bind( this );
17787         this.onKeyPressHandler = this.onKeyPress.bind( this );
17788         this.keyPressBuffer = '';
17789         this.keyPressBufferTimer = null;
17791         // Events
17792         this.connect( this, {
17793                 toggle: 'onToggle'
17794         } );
17795         this.$element.on( {
17796                 mousedown: this.onMouseDown.bind( this ),
17797                 mouseover: this.onMouseOver.bind( this ),
17798                 mouseleave: this.onMouseLeave.bind( this )
17799         } );
17801         // Initialization
17802         this.$element
17803                 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
17804                 .attr( 'role', 'listbox' );
17805         if ( Array.isArray( config.items ) ) {
17806                 this.addItems( config.items );
17807         }
17810 /* Setup */
17812 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
17814 // Need to mixin base class as well
17815 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupElement );
17816 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
17818 /* Static */
17819 OO.ui.SelectWidget.static.passAllFilter = function () {
17820         return true;
17823 /* Events */
17826  * @event highlight
17828  * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
17830  * @param {OO.ui.OptionWidget|null} item Highlighted item
17831  */
17834  * @event press
17836  * A `press` event is emitted when the #pressItem method is used to programmatically modify the
17837  * pressed state of an option.
17839  * @param {OO.ui.OptionWidget|null} item Pressed item
17840  */
17843  * @event select
17845  * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
17847  * @param {OO.ui.OptionWidget|null} item Selected item
17848  */
17851  * @event choose
17852  * A `choose` event is emitted when an item is chosen with the #chooseItem method.
17853  * @param {OO.ui.OptionWidget} item Chosen item
17854  */
17857  * @event add
17859  * An `add` event is emitted when options are added to the select with the #addItems method.
17861  * @param {OO.ui.OptionWidget[]} items Added items
17862  * @param {number} index Index of insertion point
17863  */
17866  * @event remove
17868  * A `remove` event is emitted when options are removed from the select with the #clearItems
17869  * or #removeItems methods.
17871  * @param {OO.ui.OptionWidget[]} items Removed items
17872  */
17874 /* Methods */
17877  * Handle mouse down events.
17879  * @private
17880  * @param {jQuery.Event} e Mouse down event
17881  */
17882 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
17883         var item;
17885         if ( !this.isDisabled() && e.which === 1 ) {
17886                 this.togglePressed( true );
17887                 item = this.getTargetItem( e );
17888                 if ( item && item.isSelectable() ) {
17889                         this.pressItem( item );
17890                         this.selecting = item;
17891                         OO.ui.addCaptureEventListener(
17892                                 this.getElementDocument(),
17893                                 'mouseup',
17894                                 this.onMouseUpHandler
17895                         );
17896                         OO.ui.addCaptureEventListener(
17897                                 this.getElementDocument(),
17898                                 'mousemove',
17899                                 this.onMouseMoveHandler
17900                         );
17901                 }
17902         }
17903         return false;
17907  * Handle mouse up events.
17909  * @private
17910  * @param {jQuery.Event} e Mouse up event
17911  */
17912 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
17913         var item;
17915         this.togglePressed( false );
17916         if ( !this.selecting ) {
17917                 item = this.getTargetItem( e );
17918                 if ( item && item.isSelectable() ) {
17919                         this.selecting = item;
17920                 }
17921         }
17922         if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
17923                 this.pressItem( null );
17924                 this.chooseItem( this.selecting );
17925                 this.selecting = null;
17926         }
17928         OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mouseup',
17929                 this.onMouseUpHandler );
17930         OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mousemove',
17931                 this.onMouseMoveHandler );
17933         return false;
17937  * Handle mouse move events.
17939  * @private
17940  * @param {jQuery.Event} e Mouse move event
17941  */
17942 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
17943         var item;
17945         if ( !this.isDisabled() && this.pressed ) {
17946                 item = this.getTargetItem( e );
17947                 if ( item && item !== this.selecting && item.isSelectable() ) {
17948                         this.pressItem( item );
17949                         this.selecting = item;
17950                 }
17951         }
17952         return false;
17956  * Handle mouse over events.
17958  * @private
17959  * @param {jQuery.Event} e Mouse over event
17960  */
17961 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
17962         var item;
17964         if ( !this.isDisabled() ) {
17965                 item = this.getTargetItem( e );
17966                 this.highlightItem( item && item.isHighlightable() ? item : null );
17967         }
17968         return false;
17972  * Handle mouse leave events.
17974  * @private
17975  * @param {jQuery.Event} e Mouse over event
17976  */
17977 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
17978         if ( !this.isDisabled() ) {
17979                 this.highlightItem( null );
17980         }
17981         return false;
17985  * Handle key down events.
17987  * @protected
17988  * @param {jQuery.Event} e Key down event
17989  */
17990 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
17991         var nextItem,
17992                 handled = false,
17993                 currentItem = this.getHighlightedItem() || this.getSelectedItem();
17995         if ( !this.isDisabled() && this.isVisible() ) {
17996                 switch ( e.keyCode ) {
17997                         case OO.ui.Keys.ENTER:
17998                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
17999                                         // Was only highlighted, now let's select it. No-op if already selected.
18000                                         this.chooseItem( currentItem );
18001                                         handled = true;
18002                                 }
18003                                 break;
18004                         case OO.ui.Keys.UP:
18005                         case OO.ui.Keys.LEFT:
18006                                 this.clearKeyPressBuffer();
18007                                 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
18008                                 handled = true;
18009                                 break;
18010                         case OO.ui.Keys.DOWN:
18011                         case OO.ui.Keys.RIGHT:
18012                                 this.clearKeyPressBuffer();
18013                                 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
18014                                 handled = true;
18015                                 break;
18016                         case OO.ui.Keys.ESCAPE:
18017                         case OO.ui.Keys.TAB:
18018                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
18019                                         currentItem.setHighlighted( false );
18020                                 }
18021                                 this.unbindKeyDownListener();
18022                                 this.unbindKeyPressListener();
18023                                 // Don't prevent tabbing away / defocusing
18024                                 handled = false;
18025                                 break;
18026                 }
18028                 if ( nextItem ) {
18029                         if ( nextItem.constructor.static.highlightable ) {
18030                                 this.highlightItem( nextItem );
18031                         } else {
18032                                 this.chooseItem( nextItem );
18033                         }
18034                         nextItem.scrollElementIntoView();
18035                 }
18037                 if ( handled ) {
18038                         // Can't just return false, because e is not always a jQuery event
18039                         e.preventDefault();
18040                         e.stopPropagation();
18041                 }
18042         }
18046  * Bind key down listener.
18048  * @protected
18049  */
18050 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
18051         OO.ui.addCaptureEventListener( this.getElementWindow(), 'keydown', this.onKeyDownHandler );
18055  * Unbind key down listener.
18057  * @protected
18058  */
18059 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
18060         OO.ui.removeCaptureEventListener( this.getElementWindow(), 'keydown', this.onKeyDownHandler );
18064  * Clear the key-press buffer
18066  * @protected
18067  */
18068 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
18069         if ( this.keyPressBufferTimer ) {
18070                 clearTimeout( this.keyPressBufferTimer );
18071                 this.keyPressBufferTimer = null;
18072         }
18073         this.keyPressBuffer = '';
18077  * Handle key press events.
18079  * @protected
18080  * @param {jQuery.Event} e Key press event
18081  */
18082 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
18083         var c, filter, item;
18085         if ( !e.charCode ) {
18086                 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
18087                         this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
18088                         return false;
18089                 }
18090                 return;
18091         }
18092         if ( String.fromCodePoint ) {
18093                 c = String.fromCodePoint( e.charCode );
18094         } else {
18095                 c = String.fromCharCode( e.charCode );
18096         }
18098         if ( this.keyPressBufferTimer ) {
18099                 clearTimeout( this.keyPressBufferTimer );
18100         }
18101         this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
18103         item = this.getHighlightedItem() || this.getSelectedItem();
18105         if ( this.keyPressBuffer === c ) {
18106                 // Common (if weird) special case: typing "xxxx" will cycle through all
18107                 // the items beginning with "x".
18108                 if ( item ) {
18109                         item = this.getRelativeSelectableItem( item, 1 );
18110                 }
18111         } else {
18112                 this.keyPressBuffer += c;
18113         }
18115         filter = this.getItemMatcher( this.keyPressBuffer, false );
18116         if ( !item || !filter( item ) ) {
18117                 item = this.getRelativeSelectableItem( item, 1, filter );
18118         }
18119         if ( item ) {
18120                 if ( item.constructor.static.highlightable ) {
18121                         this.highlightItem( item );
18122                 } else {
18123                         this.chooseItem( item );
18124                 }
18125                 item.scrollElementIntoView();
18126         }
18128         return false;
18132  * Get a matcher for the specific string
18134  * @protected
18135  * @param {string} s String to match against items
18136  * @param {boolean} [exact=false] Only accept exact matches
18137  * @return {Function} function ( OO.ui.OptionItem ) => boolean
18138  */
18139 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
18140         var re;
18142         if ( s.normalize ) {
18143                 s = s.normalize();
18144         }
18145         s = exact ? s.trim() : s.replace( /^\s+/, '' );
18146         re = '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
18147         if ( exact ) {
18148                 re += '\\s*$';
18149         }
18150         re = new RegExp( re, 'i' );
18151         return function ( item ) {
18152                 var l = item.getLabel();
18153                 if ( typeof l !== 'string' ) {
18154                         l = item.$label.text();
18155                 }
18156                 if ( l.normalize ) {
18157                         l = l.normalize();
18158                 }
18159                 return re.test( l );
18160         };
18164  * Bind key press listener.
18166  * @protected
18167  */
18168 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
18169         OO.ui.addCaptureEventListener( this.getElementWindow(), 'keypress', this.onKeyPressHandler );
18173  * Unbind key down listener.
18175  * If you override this, be sure to call this.clearKeyPressBuffer() from your
18176  * implementation.
18178  * @protected
18179  */
18180 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
18181         OO.ui.removeCaptureEventListener( this.getElementWindow(), 'keypress', this.onKeyPressHandler );
18182         this.clearKeyPressBuffer();
18186  * Visibility change handler
18188  * @protected
18189  * @param {boolean} visible
18190  */
18191 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
18192         if ( !visible ) {
18193                 this.clearKeyPressBuffer();
18194         }
18198  * Get the closest item to a jQuery.Event.
18200  * @private
18201  * @param {jQuery.Event} e
18202  * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
18203  */
18204 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
18205         return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
18209  * Get selected item.
18211  * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
18212  */
18213 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
18214         var i, len;
18216         for ( i = 0, len = this.items.length; i < len; i++ ) {
18217                 if ( this.items[ i ].isSelected() ) {
18218                         return this.items[ i ];
18219                 }
18220         }
18221         return null;
18225  * Get highlighted item.
18227  * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
18228  */
18229 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
18230         var i, len;
18232         for ( i = 0, len = this.items.length; i < len; i++ ) {
18233                 if ( this.items[ i ].isHighlighted() ) {
18234                         return this.items[ i ];
18235                 }
18236         }
18237         return null;
18241  * Toggle pressed state.
18243  * Press is a state that occurs when a user mouses down on an item, but
18244  * has not yet let go of the mouse. The item may appear selected, but it will not be selected
18245  * until the user releases the mouse.
18247  * @param {boolean} pressed An option is being pressed
18248  */
18249 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
18250         if ( pressed === undefined ) {
18251                 pressed = !this.pressed;
18252         }
18253         if ( pressed !== this.pressed ) {
18254                 this.$element
18255                         .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
18256                         .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
18257                 this.pressed = pressed;
18258         }
18262  * Highlight an option. If the `item` param is omitted, no options will be highlighted
18263  * and any existing highlight will be removed. The highlight is mutually exclusive.
18265  * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
18266  * @fires highlight
18267  * @chainable
18268  */
18269 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
18270         var i, len, highlighted,
18271                 changed = false;
18273         for ( i = 0, len = this.items.length; i < len; i++ ) {
18274                 highlighted = this.items[ i ] === item;
18275                 if ( this.items[ i ].isHighlighted() !== highlighted ) {
18276                         this.items[ i ].setHighlighted( highlighted );
18277                         changed = true;
18278                 }
18279         }
18280         if ( changed ) {
18281                 this.emit( 'highlight', item );
18282         }
18284         return this;
18288  * Fetch an item by its label.
18290  * @param {string} label Label of the item to select.
18291  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
18292  * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
18293  */
18294 OO.ui.SelectWidget.prototype.getItemFromLabel = function ( label, prefix ) {
18295         var i, item, found,
18296                 len = this.items.length,
18297                 filter = this.getItemMatcher( label, true );
18299         for ( i = 0; i < len; i++ ) {
18300                 item = this.items[ i ];
18301                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
18302                         return item;
18303                 }
18304         }
18306         if ( prefix ) {
18307                 found = null;
18308                 filter = this.getItemMatcher( label, false );
18309                 for ( i = 0; i < len; i++ ) {
18310                         item = this.items[ i ];
18311                         if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
18312                                 if ( found ) {
18313                                         return null;
18314                                 }
18315                                 found = item;
18316                         }
18317                 }
18318                 if ( found ) {
18319                         return found;
18320                 }
18321         }
18323         return null;
18327  * Programmatically select an option by its label. If the item does not exist,
18328  * all options will be deselected.
18330  * @param {string} [label] Label of the item to select.
18331  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
18332  * @fires select
18333  * @chainable
18334  */
18335 OO.ui.SelectWidget.prototype.selectItemByLabel = function ( label, prefix ) {
18336         var itemFromLabel = this.getItemFromLabel( label, !!prefix );
18337         if ( label === undefined || !itemFromLabel ) {
18338                 return this.selectItem();
18339         }
18340         return this.selectItem( itemFromLabel );
18344  * Programmatically select an option by its data. If the `data` parameter is omitted,
18345  * or if the item does not exist, all options will be deselected.
18347  * @param {Object|string} [data] Value of the item to select, omit to deselect all
18348  * @fires select
18349  * @chainable
18350  */
18351 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
18352         var itemFromData = this.getItemFromData( data );
18353         if ( data === undefined || !itemFromData ) {
18354                 return this.selectItem();
18355         }
18356         return this.selectItem( itemFromData );
18360  * Programmatically select an option by its reference. If the `item` parameter is omitted,
18361  * all options will be deselected.
18363  * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
18364  * @fires select
18365  * @chainable
18366  */
18367 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
18368         var i, len, selected,
18369                 changed = false;
18371         for ( i = 0, len = this.items.length; i < len; i++ ) {
18372                 selected = this.items[ i ] === item;
18373                 if ( this.items[ i ].isSelected() !== selected ) {
18374                         this.items[ i ].setSelected( selected );
18375                         changed = true;
18376                 }
18377         }
18378         if ( changed ) {
18379                 this.emit( 'select', item );
18380         }
18382         return this;
18386  * Press an item.
18388  * Press is a state that occurs when a user mouses down on an item, but has not
18389  * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
18390  * releases the mouse.
18392  * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
18393  * @fires press
18394  * @chainable
18395  */
18396 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
18397         var i, len, pressed,
18398                 changed = false;
18400         for ( i = 0, len = this.items.length; i < len; i++ ) {
18401                 pressed = this.items[ i ] === item;
18402                 if ( this.items[ i ].isPressed() !== pressed ) {
18403                         this.items[ i ].setPressed( pressed );
18404                         changed = true;
18405                 }
18406         }
18407         if ( changed ) {
18408                 this.emit( 'press', item );
18409         }
18411         return this;
18415  * Choose an item.
18417  * Note that ‘choose’ should never be modified programmatically. A user can choose
18418  * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
18419  * use the #selectItem method.
18421  * This method is identical to #selectItem, but may vary in subclasses that take additional action
18422  * when users choose an item with the keyboard or mouse.
18424  * @param {OO.ui.OptionWidget} item Item to choose
18425  * @fires choose
18426  * @chainable
18427  */
18428 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
18429         if ( item ) {
18430                 this.selectItem( item );
18431                 this.emit( 'choose', item );
18432         }
18434         return this;
18438  * Get an option by its position relative to the specified item (or to the start of the option array,
18439  * if item is `null`). The direction in which to search through the option array is specified with a
18440  * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
18441  * `null` if there are no options in the array.
18443  * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
18444  * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
18445  * @param {Function} filter Only consider items for which this function returns
18446  *  true. Function takes an OO.ui.OptionWidget and returns a boolean.
18447  * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
18448  */
18449 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
18450         var currentIndex, nextIndex, i,
18451                 increase = direction > 0 ? 1 : -1,
18452                 len = this.items.length;
18454         if ( !$.isFunction( filter ) ) {
18455                 filter = OO.ui.SelectWidget.static.passAllFilter;
18456         }
18458         if ( item instanceof OO.ui.OptionWidget ) {
18459                 currentIndex = this.items.indexOf( item );
18460                 nextIndex = ( currentIndex + increase + len ) % len;
18461         } else {
18462                 // If no item is selected and moving forward, start at the beginning.
18463                 // If moving backward, start at the end.
18464                 nextIndex = direction > 0 ? 0 : len - 1;
18465         }
18467         for ( i = 0; i < len; i++ ) {
18468                 item = this.items[ nextIndex ];
18469                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
18470                         return item;
18471                 }
18472                 nextIndex = ( nextIndex + increase + len ) % len;
18473         }
18474         return null;
18478  * Get the next selectable item or `null` if there are no selectable items.
18479  * Disabled options and menu-section markers and breaks are not selectable.
18481  * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
18482  */
18483 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
18484         var i, len, item;
18486         for ( i = 0, len = this.items.length; i < len; i++ ) {
18487                 item = this.items[ i ];
18488                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
18489                         return item;
18490                 }
18491         }
18493         return null;
18497  * Add an array of options to the select. Optionally, an index number can be used to
18498  * specify an insertion point.
18500  * @param {OO.ui.OptionWidget[]} items Items to add
18501  * @param {number} [index] Index to insert items after
18502  * @fires add
18503  * @chainable
18504  */
18505 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
18506         // Mixin method
18507         OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
18509         // Always provide an index, even if it was omitted
18510         this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
18512         return this;
18516  * Remove the specified array of options from the select. Options will be detached
18517  * from the DOM, not removed, so they can be reused later. To remove all options from
18518  * the select, you may wish to use the #clearItems method instead.
18520  * @param {OO.ui.OptionWidget[]} items Items to remove
18521  * @fires remove
18522  * @chainable
18523  */
18524 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
18525         var i, len, item;
18527         // Deselect items being removed
18528         for ( i = 0, len = items.length; i < len; i++ ) {
18529                 item = items[ i ];
18530                 if ( item.isSelected() ) {
18531                         this.selectItem( null );
18532                 }
18533         }
18535         // Mixin method
18536         OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
18538         this.emit( 'remove', items );
18540         return this;
18544  * Clear all options from the select. Options will be detached from the DOM, not removed,
18545  * so that they can be reused later. To remove a subset of options from the select, use
18546  * the #removeItems method.
18548  * @fires remove
18549  * @chainable
18550  */
18551 OO.ui.SelectWidget.prototype.clearItems = function () {
18552         var items = this.items.slice();
18554         // Mixin method
18555         OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
18557         // Clear selection
18558         this.selectItem( null );
18560         this.emit( 'remove', items );
18562         return this;
18566  * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
18567  * button options and is used together with
18568  * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
18569  * highlighting, choosing, and selecting mutually exclusive options. Please see
18570  * the [OOjs UI documentation on MediaWiki] [1] for more information.
18572  *     @example
18573  *     // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
18574  *     var option1 = new OO.ui.ButtonOptionWidget( {
18575  *         data: 1,
18576  *         label: 'Option 1',
18577  *         title: 'Button option 1'
18578  *     } );
18580  *     var option2 = new OO.ui.ButtonOptionWidget( {
18581  *         data: 2,
18582  *         label: 'Option 2',
18583  *         title: 'Button option 2'
18584  *     } );
18586  *     var option3 = new OO.ui.ButtonOptionWidget( {
18587  *         data: 3,
18588  *         label: 'Option 3',
18589  *         title: 'Button option 3'
18590  *     } );
18592  *     var buttonSelect=new OO.ui.ButtonSelectWidget( {
18593  *         items: [ option1, option2, option3 ]
18594  *     } );
18595  *     $( 'body' ).append( buttonSelect.$element );
18597  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
18599  * @class
18600  * @extends OO.ui.SelectWidget
18601  * @mixins OO.ui.mixin.TabIndexedElement
18603  * @constructor
18604  * @param {Object} [config] Configuration options
18605  */
18606 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
18607         // Parent constructor
18608         OO.ui.ButtonSelectWidget.parent.call( this, config );
18610         // Mixin constructors
18611         OO.ui.mixin.TabIndexedElement.call( this, config );
18613         // Events
18614         this.$element.on( {
18615                 focus: this.bindKeyDownListener.bind( this ),
18616                 blur: this.unbindKeyDownListener.bind( this )
18617         } );
18619         // Initialization
18620         this.$element.addClass( 'oo-ui-buttonSelectWidget' );
18623 /* Setup */
18625 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
18626 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
18629  * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
18630  * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
18631  * an interface for adding, removing and selecting options.
18632  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
18634  * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
18635  * OO.ui.RadioSelectInputWidget instead.
18637  *     @example
18638  *     // A RadioSelectWidget with RadioOptions.
18639  *     var option1 = new OO.ui.RadioOptionWidget( {
18640  *         data: 'a',
18641  *         label: 'Selected radio option'
18642  *     } );
18644  *     var option2 = new OO.ui.RadioOptionWidget( {
18645  *         data: 'b',
18646  *         label: 'Unselected radio option'
18647  *     } );
18649  *     var radioSelect=new OO.ui.RadioSelectWidget( {
18650  *         items: [ option1, option2 ]
18651  *      } );
18653  *     // Select 'option 1' using the RadioSelectWidget's selectItem() method.
18654  *     radioSelect.selectItem( option1 );
18656  *     $( 'body' ).append( radioSelect.$element );
18658  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
18661  * @class
18662  * @extends OO.ui.SelectWidget
18663  * @mixins OO.ui.mixin.TabIndexedElement
18665  * @constructor
18666  * @param {Object} [config] Configuration options
18667  */
18668 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
18669         // Parent constructor
18670         OO.ui.RadioSelectWidget.parent.call( this, config );
18672         // Mixin constructors
18673         OO.ui.mixin.TabIndexedElement.call( this, config );
18675         // Events
18676         this.$element.on( {
18677                 focus: this.bindKeyDownListener.bind( this ),
18678                 blur: this.unbindKeyDownListener.bind( this )
18679         } );
18681         // Initialization
18682         this.$element
18683                 .addClass( 'oo-ui-radioSelectWidget' )
18684                 .attr( 'role', 'radiogroup' );
18687 /* Setup */
18689 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
18690 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
18693  * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
18694  * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
18695  * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxWidget ComboBoxWidget},
18696  * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
18697  * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
18698  * and customized to be opened, closed, and displayed as needed.
18700  * By default, menus are clipped to the visible viewport and are not visible when a user presses the
18701  * mouse outside the menu.
18703  * Menus also have support for keyboard interaction:
18705  * - Enter/Return key: choose and select a menu option
18706  * - Up-arrow key: highlight the previous menu option
18707  * - Down-arrow key: highlight the next menu option
18708  * - Esc key: hide the menu
18710  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
18711  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
18713  * @class
18714  * @extends OO.ui.SelectWidget
18715  * @mixins OO.ui.mixin.ClippableElement
18717  * @constructor
18718  * @param {Object} [config] Configuration options
18719  * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
18720  *  the text the user types. This config is used by {@link OO.ui.ComboBoxWidget ComboBoxWidget}
18721  *  and {@link OO.ui.mixin.LookupElement LookupElement}
18722  * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
18723  *  the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
18724  * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
18725  *  anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
18726  *  that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
18727  *  that button, unless the button (or its parent widget) is passed in here.
18728  * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
18729  * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
18730  */
18731 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
18732         // Configuration initialization
18733         config = config || {};
18735         // Parent constructor
18736         OO.ui.MenuSelectWidget.parent.call( this, config );
18738         // Mixin constructors
18739         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
18741         // Properties
18742         this.newItems = null;
18743         this.autoHide = config.autoHide === undefined || !!config.autoHide;
18744         this.filterFromInput = !!config.filterFromInput;
18745         this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
18746         this.$widget = config.widget ? config.widget.$element : null;
18747         this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
18748         this.onInputEditHandler = OO.ui.debounce( this.updateItemVisibility.bind( this ), 100 );
18750         // Initialization
18751         this.$element
18752                 .addClass( 'oo-ui-menuSelectWidget' )
18753                 .attr( 'role', 'menu' );
18755         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
18756         // that reference properties not initialized at that time of parent class construction
18757         // TODO: Find a better way to handle post-constructor setup
18758         this.visible = false;
18759         this.$element.addClass( 'oo-ui-element-hidden' );
18762 /* Setup */
18764 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
18765 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
18767 /* Methods */
18770  * Handles document mouse down events.
18772  * @protected
18773  * @param {jQuery.Event} e Key down event
18774  */
18775 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
18776         if (
18777                 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
18778                 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
18779         ) {
18780                 this.toggle( false );
18781         }
18785  * @inheritdoc
18786  */
18787 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
18788         var currentItem = this.getHighlightedItem() || this.getSelectedItem();
18790         if ( !this.isDisabled() && this.isVisible() ) {
18791                 switch ( e.keyCode ) {
18792                         case OO.ui.Keys.LEFT:
18793                         case OO.ui.Keys.RIGHT:
18794                                 // Do nothing if a text field is associated, arrow keys will be handled natively
18795                                 if ( !this.$input ) {
18796                                         OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
18797                                 }
18798                                 break;
18799                         case OO.ui.Keys.ESCAPE:
18800                         case OO.ui.Keys.TAB:
18801                                 if ( currentItem ) {
18802                                         currentItem.setHighlighted( false );
18803                                 }
18804                                 this.toggle( false );
18805                                 // Don't prevent tabbing away, prevent defocusing
18806                                 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
18807                                         e.preventDefault();
18808                                         e.stopPropagation();
18809                                 }
18810                                 break;
18811                         default:
18812                                 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
18813                                 return;
18814                 }
18815         }
18819  * Update menu item visibility after input changes.
18820  * @protected
18821  */
18822 OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
18823         var i, item,
18824                 len = this.items.length,
18825                 showAll = !this.isVisible(),
18826                 filter = showAll ? null : this.getItemMatcher( this.$input.val() );
18828         for ( i = 0; i < len; i++ ) {
18829                 item = this.items[ i ];
18830                 if ( item instanceof OO.ui.OptionWidget ) {
18831                         item.toggle( showAll || filter( item ) );
18832                 }
18833         }
18835         // Reevaluate clipping
18836         this.clip();
18840  * @inheritdoc
18841  */
18842 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
18843         if ( this.$input ) {
18844                 this.$input.on( 'keydown', this.onKeyDownHandler );
18845         } else {
18846                 OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
18847         }
18851  * @inheritdoc
18852  */
18853 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
18854         if ( this.$input ) {
18855                 this.$input.off( 'keydown', this.onKeyDownHandler );
18856         } else {
18857                 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
18858         }
18862  * @inheritdoc
18863  */
18864 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
18865         if ( this.$input ) {
18866                 if ( this.filterFromInput ) {
18867                         this.$input.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
18868                 }
18869         } else {
18870                 OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
18871         }
18875  * @inheritdoc
18876  */
18877 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
18878         if ( this.$input ) {
18879                 if ( this.filterFromInput ) {
18880                         this.$input.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
18881                         this.updateItemVisibility();
18882                 }
18883         } else {
18884                 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
18885         }
18889  * Choose an item.
18891  * When a user chooses an item, the menu is closed.
18893  * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
18894  * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
18895  * @param {OO.ui.OptionWidget} item Item to choose
18896  * @chainable
18897  */
18898 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
18899         OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
18900         this.toggle( false );
18901         return this;
18905  * @inheritdoc
18906  */
18907 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
18908         var i, len, item;
18910         // Parent method
18911         OO.ui.MenuSelectWidget.parent.prototype.addItems.call( this, items, index );
18913         // Auto-initialize
18914         if ( !this.newItems ) {
18915                 this.newItems = [];
18916         }
18918         for ( i = 0, len = items.length; i < len; i++ ) {
18919                 item = items[ i ];
18920                 if ( this.isVisible() ) {
18921                         // Defer fitting label until item has been attached
18922                         item.fitLabel();
18923                 } else {
18924                         this.newItems.push( item );
18925                 }
18926         }
18928         // Reevaluate clipping
18929         this.clip();
18931         return this;
18935  * @inheritdoc
18936  */
18937 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
18938         // Parent method
18939         OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
18941         // Reevaluate clipping
18942         this.clip();
18944         return this;
18948  * @inheritdoc
18949  */
18950 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
18951         // Parent method
18952         OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
18954         // Reevaluate clipping
18955         this.clip();
18957         return this;
18961  * @inheritdoc
18962  */
18963 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
18964         var i, len, change;
18966         visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
18967         change = visible !== this.isVisible();
18969         // Parent method
18970         OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
18972         if ( change ) {
18973                 if ( visible ) {
18974                         this.bindKeyDownListener();
18975                         this.bindKeyPressListener();
18977                         if ( this.newItems && this.newItems.length ) {
18978                                 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
18979                                         this.newItems[ i ].fitLabel();
18980                                 }
18981                                 this.newItems = null;
18982                         }
18983                         this.toggleClipping( true );
18985                         // Auto-hide
18986                         if ( this.autoHide ) {
18987                                 OO.ui.addCaptureEventListener( this.getElementDocument(), 'mousedown', this.onDocumentMouseDownHandler );
18988                         }
18989                 } else {
18990                         this.unbindKeyDownListener();
18991                         this.unbindKeyPressListener();
18992                         OO.ui.removeCaptureEventListener( this.getElementDocument(), 'mousedown', this.onDocumentMouseDownHandler );
18993                         this.toggleClipping( false );
18994                 }
18995         }
18997         return this;
19001  * FloatingMenuSelectWidget is a menu that will stick under a specified
19002  * container, even when it is inserted elsewhere in the document (for example,
19003  * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
19004  * menu from being clipped too aggresively.
19006  * The menu's position is automatically calculated and maintained when the menu
19007  * is toggled or the window is resized.
19009  * See OO.ui.ComboBoxWidget for an example of a widget that uses this class.
19011  * @class
19012  * @extends OO.ui.MenuSelectWidget
19013  * @mixins OO.ui.mixin.FloatableElement
19015  * @constructor
19016  * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
19017  *   Deprecated, omit this parameter and specify `$container` instead.
19018  * @param {Object} [config] Configuration options
19019  * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
19020  */
19021 OO.ui.FloatingMenuSelectWidget = function OoUiFloatingMenuSelectWidget( inputWidget, config ) {
19022         // Allow 'inputWidget' parameter and config for backwards compatibility
19023         if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
19024                 config = inputWidget;
19025                 inputWidget = config.inputWidget;
19026         }
19028         // Configuration initialization
19029         config = config || {};
19031         // Parent constructor
19032         OO.ui.FloatingMenuSelectWidget.parent.call( this, config );
19034         // Properties (must be set before mixin constructors)
19035         this.inputWidget = inputWidget; // For backwards compatibility
19036         this.$container = config.$container || this.inputWidget.$element;
19038         // Mixins constructors
19039         OO.ui.mixin.FloatableElement.call( this, $.extend( {}, config, { $floatableContainer: this.$container } ) );
19041         // Initialization
19042         this.$element.addClass( 'oo-ui-floatingMenuSelectWidget' );
19043         // For backwards compatibility
19044         this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
19047 /* Setup */
19049 OO.inheritClass( OO.ui.FloatingMenuSelectWidget, OO.ui.MenuSelectWidget );
19050 OO.mixinClass( OO.ui.FloatingMenuSelectWidget, OO.ui.mixin.FloatableElement );
19052 // For backwards compatibility
19053 OO.ui.TextInputMenuSelectWidget = OO.ui.FloatingMenuSelectWidget;
19055 /* Methods */
19058  * @inheritdoc
19059  */
19060 OO.ui.FloatingMenuSelectWidget.prototype.toggle = function ( visible ) {
19061         var change;
19062         visible = visible === undefined ? !this.isVisible() : !!visible;
19063         change = visible !== this.isVisible();
19065         if ( change && visible ) {
19066                 // Make sure the width is set before the parent method runs.
19067                 this.setIdealSize( this.$container.width() );
19068         }
19070         // Parent method
19071         // This will call this.clip(), which is nonsensical since we're not positioned yet...
19072         OO.ui.FloatingMenuSelectWidget.parent.prototype.toggle.call( this, visible );
19074         if ( change ) {
19075                 this.togglePositioning( this.isVisible() );
19076         }
19078         return this;
19082  * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
19083  * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
19085  * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
19087  * @class
19088  * @extends OO.ui.SelectWidget
19089  * @mixins OO.ui.mixin.TabIndexedElement
19091  * @constructor
19092  * @param {Object} [config] Configuration options
19093  */
19094 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
19095         // Parent constructor
19096         OO.ui.OutlineSelectWidget.parent.call( this, config );
19098         // Mixin constructors
19099         OO.ui.mixin.TabIndexedElement.call( this, config );
19101         // Events
19102         this.$element.on( {
19103                 focus: this.bindKeyDownListener.bind( this ),
19104                 blur: this.unbindKeyDownListener.bind( this )
19105         } );
19107         // Initialization
19108         this.$element.addClass( 'oo-ui-outlineSelectWidget' );
19111 /* Setup */
19113 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
19114 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
19117  * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
19119  * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
19121  * @class
19122  * @extends OO.ui.SelectWidget
19123  * @mixins OO.ui.mixin.TabIndexedElement
19125  * @constructor
19126  * @param {Object} [config] Configuration options
19127  */
19128 OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
19129         // Parent constructor
19130         OO.ui.TabSelectWidget.parent.call( this, config );
19132         // Mixin constructors
19133         OO.ui.mixin.TabIndexedElement.call( this, config );
19135         // Events
19136         this.$element.on( {
19137                 focus: this.bindKeyDownListener.bind( this ),
19138                 blur: this.unbindKeyDownListener.bind( this )
19139         } );
19141         // Initialization
19142         this.$element.addClass( 'oo-ui-tabSelectWidget' );
19145 /* Setup */
19147 OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
19148 OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
19151  * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
19152  * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
19153  * (to adjust the value in increments) to allow the user to enter a number.
19155  *     @example
19156  *     // Example: A NumberInputWidget.
19157  *     var numberInput = new OO.ui.NumberInputWidget( {
19158  *         label: 'NumberInputWidget',
19159  *         input: { value: 5, min: 1, max: 10 }
19160  *     } );
19161  *     $( 'body' ).append( numberInput.$element );
19163  * @class
19164  * @extends OO.ui.Widget
19166  * @constructor
19167  * @param {Object} [config] Configuration options
19168  * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
19169  * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
19170  * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
19171  * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
19172  * @cfg {number} [min=-Infinity] Minimum allowed value
19173  * @cfg {number} [max=Infinity] Maximum allowed value
19174  * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
19175  * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
19176  */
19177 OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
19178         // Configuration initialization
19179         config = $.extend( {
19180                 isInteger: false,
19181                 min: -Infinity,
19182                 max: Infinity,
19183                 step: 1,
19184                 pageStep: null
19185         }, config );
19187         // Parent constructor
19188         OO.ui.NumberInputWidget.parent.call( this, config );
19190         // Properties
19191         this.input = new OO.ui.TextInputWidget( $.extend(
19192                 {
19193                         disabled: this.isDisabled()
19194                 },
19195                 config.input
19196         ) );
19197         this.minusButton = new OO.ui.ButtonWidget( $.extend(
19198                 {
19199                         disabled: this.isDisabled(),
19200                         tabIndex: -1
19201                 },
19202                 config.minusButton,
19203                 {
19204                         classes: [ 'oo-ui-numberInputWidget-minusButton' ],
19205                         label: '−'
19206                 }
19207         ) );
19208         this.plusButton = new OO.ui.ButtonWidget( $.extend(
19209                 {
19210                         disabled: this.isDisabled(),
19211                         tabIndex: -1
19212                 },
19213                 config.plusButton,
19214                 {
19215                         classes: [ 'oo-ui-numberInputWidget-plusButton' ],
19216                         label: '+'
19217                 }
19218         ) );
19220         // Events
19221         this.input.connect( this, {
19222                 change: this.emit.bind( this, 'change' ),
19223                 enter: this.emit.bind( this, 'enter' )
19224         } );
19225         this.input.$input.on( {
19226                 keydown: this.onKeyDown.bind( this ),
19227                 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
19228         } );
19229         this.plusButton.connect( this, {
19230                 click: [ 'onButtonClick', +1 ]
19231         } );
19232         this.minusButton.connect( this, {
19233                 click: [ 'onButtonClick', -1 ]
19234         } );
19236         // Initialization
19237         this.setIsInteger( !!config.isInteger );
19238         this.setRange( config.min, config.max );
19239         this.setStep( config.step, config.pageStep );
19241         this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
19242                 .append(
19243                         this.minusButton.$element,
19244                         this.input.$element,
19245                         this.plusButton.$element
19246                 );
19247         this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
19248         this.input.setValidation( this.validateNumber.bind( this ) );
19251 /* Setup */
19253 OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
19255 /* Events */
19258  * A `change` event is emitted when the value of the input changes.
19260  * @event change
19261  */
19264  * An `enter` event is emitted when the user presses 'enter' inside the text box.
19266  * @event enter
19267  */
19269 /* Methods */
19272  * Set whether only integers are allowed
19273  * @param {boolean} flag
19274  */
19275 OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
19276         this.isInteger = !!flag;
19277         this.input.setValidityFlag();
19281  * Get whether only integers are allowed
19282  * @return {boolean} Flag value
19283  */
19284 OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
19285         return this.isInteger;
19289  * Set the range of allowed values
19290  * @param {number} min Minimum allowed value
19291  * @param {number} max Maximum allowed value
19292  */
19293 OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
19294         if ( min > max ) {
19295                 throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
19296         }
19297         this.min = min;
19298         this.max = max;
19299         this.input.setValidityFlag();
19303  * Get the current range
19304  * @return {number[]} Minimum and maximum values
19305  */
19306 OO.ui.NumberInputWidget.prototype.getRange = function () {
19307         return [ this.min, this.max ];
19311  * Set the stepping deltas
19312  * @param {number} step Normal step
19313  * @param {number|null} pageStep Page step. If null, 10 * step will be used.
19314  */
19315 OO.ui.NumberInputWidget.prototype.setStep = function ( step, pageStep ) {
19316         if ( step <= 0 ) {
19317                 throw new Error( 'Step value must be positive' );
19318         }
19319         if ( pageStep === null ) {
19320                 pageStep = step * 10;
19321         } else if ( pageStep <= 0 ) {
19322                 throw new Error( 'Page step value must be positive' );
19323         }
19324         this.step = step;
19325         this.pageStep = pageStep;
19329  * Get the current stepping values
19330  * @return {number[]} Step and page step
19331  */
19332 OO.ui.NumberInputWidget.prototype.getStep = function () {
19333         return [ this.step, this.pageStep ];
19337  * Get the current value of the widget
19338  * @return {string}
19339  */
19340 OO.ui.NumberInputWidget.prototype.getValue = function () {
19341         return this.input.getValue();
19345  * Get the current value of the widget as a number
19346  * @return {number} May be NaN, or an invalid number
19347  */
19348 OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
19349         return +this.input.getValue();
19353  * Set the value of the widget
19354  * @param {string} value Invalid values are allowed
19355  */
19356 OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
19357         this.input.setValue( value );
19361  * Adjust the value of the widget
19362  * @param {number} delta Adjustment amount
19363  */
19364 OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
19365         var n, v = this.getNumericValue();
19367         delta = +delta;
19368         if ( isNaN( delta ) || !isFinite( delta ) ) {
19369                 throw new Error( 'Delta must be a finite number' );
19370         }
19372         if ( isNaN( v ) ) {
19373                 n = 0;
19374         } else {
19375                 n = v + delta;
19376                 n = Math.max( Math.min( n, this.max ), this.min );
19377                 if ( this.isInteger ) {
19378                         n = Math.round( n );
19379                 }
19380         }
19382         if ( n !== v ) {
19383                 this.setValue( n );
19384         }
19388  * Validate input
19389  * @private
19390  * @param {string} value Field value
19391  * @return {boolean}
19392  */
19393 OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
19394         var n = +value;
19395         if ( isNaN( n ) || !isFinite( n ) ) {
19396                 return false;
19397         }
19399         /*jshint bitwise: false */
19400         if ( this.isInteger && ( n | 0 ) !== n ) {
19401                 return false;
19402         }
19403         /*jshint bitwise: true */
19405         if ( n < this.min || n > this.max ) {
19406                 return false;
19407         }
19409         return true;
19413  * Handle mouse click events.
19415  * @private
19416  * @param {number} dir +1 or -1
19417  */
19418 OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
19419         this.adjustValue( dir * this.step );
19423  * Handle mouse wheel events.
19425  * @private
19426  * @param {jQuery.Event} event
19427  */
19428 OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
19429         var delta = 0;
19431         // Standard 'wheel' event
19432         if ( event.originalEvent.deltaMode !== undefined ) {
19433                 this.sawWheelEvent = true;
19434         }
19435         if ( event.originalEvent.deltaY ) {
19436                 delta = -event.originalEvent.deltaY;
19437         } else if ( event.originalEvent.deltaX ) {
19438                 delta = event.originalEvent.deltaX;
19439         }
19441         // Non-standard events
19442         if ( !this.sawWheelEvent ) {
19443                 if ( event.originalEvent.wheelDeltaX ) {
19444                         delta = -event.originalEvent.wheelDeltaX;
19445                 } else if ( event.originalEvent.wheelDeltaY ) {
19446                         delta = event.originalEvent.wheelDeltaY;
19447                 } else if ( event.originalEvent.wheelDelta ) {
19448                         delta = event.originalEvent.wheelDelta;
19449                 } else if ( event.originalEvent.detail ) {
19450                         delta = -event.originalEvent.detail;
19451                 }
19452         }
19454         if ( delta ) {
19455                 delta = delta < 0 ? -1 : 1;
19456                 this.adjustValue( delta * this.step );
19457         }
19459         return false;
19463  * Handle key down events.
19465  * @private
19466  * @param {jQuery.Event} e Key down event
19467  */
19468 OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
19469         if ( !this.isDisabled() ) {
19470                 switch ( e.which ) {
19471                         case OO.ui.Keys.UP:
19472                                 this.adjustValue( this.step );
19473                                 return false;
19474                         case OO.ui.Keys.DOWN:
19475                                 this.adjustValue( -this.step );
19476                                 return false;
19477                         case OO.ui.Keys.PAGEUP:
19478                                 this.adjustValue( this.pageStep );
19479                                 return false;
19480                         case OO.ui.Keys.PAGEDOWN:
19481                                 this.adjustValue( -this.pageStep );
19482                                 return false;
19483                 }
19484         }
19488  * @inheritdoc
19489  */
19490 OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
19491         // Parent method
19492         OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
19494         if ( this.input ) {
19495                 this.input.setDisabled( this.isDisabled() );
19496         }
19497         if ( this.minusButton ) {
19498                 this.minusButton.setDisabled( this.isDisabled() );
19499         }
19500         if ( this.plusButton ) {
19501                 this.plusButton.setDisabled( this.isDisabled() );
19502         }
19504         return this;
19508  * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
19509  * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
19510  * visually by a slider in the leftmost position.
19512  *     @example
19513  *     // Toggle switches in the 'off' and 'on' position.
19514  *     var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
19515  *     var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
19516  *         value: true
19517  *     } );
19519  *     // Create a FieldsetLayout to layout and label switches
19520  *     var fieldset = new OO.ui.FieldsetLayout( {
19521  *        label: 'Toggle switches'
19522  *     } );
19523  *     fieldset.addItems( [
19524  *         new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
19525  *         new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
19526  *     ] );
19527  *     $( 'body' ).append( fieldset.$element );
19529  * @class
19530  * @extends OO.ui.ToggleWidget
19531  * @mixins OO.ui.mixin.TabIndexedElement
19533  * @constructor
19534  * @param {Object} [config] Configuration options
19535  * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
19536  *  By default, the toggle switch is in the 'off' position.
19537  */
19538 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
19539         // Parent constructor
19540         OO.ui.ToggleSwitchWidget.parent.call( this, config );
19542         // Mixin constructors
19543         OO.ui.mixin.TabIndexedElement.call( this, config );
19545         // Properties
19546         this.dragging = false;
19547         this.dragStart = null;
19548         this.sliding = false;
19549         this.$glow = $( '<span>' );
19550         this.$grip = $( '<span>' );
19552         // Events
19553         this.$element.on( {
19554                 click: this.onClick.bind( this ),
19555                 keypress: this.onKeyPress.bind( this )
19556         } );
19558         // Initialization
19559         this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
19560         this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
19561         this.$element
19562                 .addClass( 'oo-ui-toggleSwitchWidget' )
19563                 .attr( 'role', 'checkbox' )
19564                 .append( this.$glow, this.$grip );
19567 /* Setup */
19569 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
19570 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
19572 /* Methods */
19575  * Handle mouse click events.
19577  * @private
19578  * @param {jQuery.Event} e Mouse click event
19579  */
19580 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
19581         if ( !this.isDisabled() && e.which === 1 ) {
19582                 this.setValue( !this.value );
19583         }
19584         return false;
19588  * Handle key press events.
19590  * @private
19591  * @param {jQuery.Event} e Key press event
19592  */
19593 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
19594         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
19595                 this.setValue( !this.value );
19596                 return false;
19597         }
19601  * Deprecated aliases for classes in the `OO.ui.mixin` namespace.
19602  */
19605  * @inheritdoc OO.ui.mixin.ButtonElement
19606  * @deprecated Use {@link OO.ui.mixin.ButtonElement} instead.
19607  */
19608 OO.ui.ButtonElement = OO.ui.mixin.ButtonElement;
19611  * @inheritdoc OO.ui.mixin.ClippableElement
19612  * @deprecated Use {@link OO.ui.mixin.ClippableElement} instead.
19613  */
19614 OO.ui.ClippableElement = OO.ui.mixin.ClippableElement;
19617  * @inheritdoc OO.ui.mixin.DraggableElement
19618  * @deprecated Use {@link OO.ui.mixin.DraggableElement} instead.
19619  */
19620 OO.ui.DraggableElement = OO.ui.mixin.DraggableElement;
19623  * @inheritdoc OO.ui.mixin.DraggableGroupElement
19624  * @deprecated Use {@link OO.ui.mixin.DraggableGroupElement} instead.
19625  */
19626 OO.ui.DraggableGroupElement = OO.ui.mixin.DraggableGroupElement;
19629  * @inheritdoc OO.ui.mixin.FlaggedElement
19630  * @deprecated Use {@link OO.ui.mixin.FlaggedElement} instead.
19631  */
19632 OO.ui.FlaggedElement = OO.ui.mixin.FlaggedElement;
19635  * @inheritdoc OO.ui.mixin.GroupElement
19636  * @deprecated Use {@link OO.ui.mixin.GroupElement} instead.
19637  */
19638 OO.ui.GroupElement = OO.ui.mixin.GroupElement;
19641  * @inheritdoc OO.ui.mixin.GroupWidget
19642  * @deprecated Use {@link OO.ui.mixin.GroupWidget} instead.
19643  */
19644 OO.ui.GroupWidget = OO.ui.mixin.GroupWidget;
19647  * @inheritdoc OO.ui.mixin.IconElement
19648  * @deprecated Use {@link OO.ui.mixin.IconElement} instead.
19649  */
19650 OO.ui.IconElement = OO.ui.mixin.IconElement;
19653  * @inheritdoc OO.ui.mixin.IndicatorElement
19654  * @deprecated Use {@link OO.ui.mixin.IndicatorElement} instead.
19655  */
19656 OO.ui.IndicatorElement = OO.ui.mixin.IndicatorElement;
19659  * @inheritdoc OO.ui.mixin.ItemWidget
19660  * @deprecated Use {@link OO.ui.mixin.ItemWidget} instead.
19661  */
19662 OO.ui.ItemWidget = OO.ui.mixin.ItemWidget;
19665  * @inheritdoc OO.ui.mixin.LabelElement
19666  * @deprecated Use {@link OO.ui.mixin.LabelElement} instead.
19667  */
19668 OO.ui.LabelElement = OO.ui.mixin.LabelElement;
19671  * @inheritdoc OO.ui.mixin.LookupElement
19672  * @deprecated Use {@link OO.ui.mixin.LookupElement} instead.
19673  */
19674 OO.ui.LookupElement = OO.ui.mixin.LookupElement;
19677  * @inheritdoc OO.ui.mixin.PendingElement
19678  * @deprecated Use {@link OO.ui.mixin.PendingElement} instead.
19679  */
19680 OO.ui.PendingElement = OO.ui.mixin.PendingElement;
19683  * @inheritdoc OO.ui.mixin.PopupElement
19684  * @deprecated Use {@link OO.ui.mixin.PopupElement} instead.
19685  */
19686 OO.ui.PopupElement = OO.ui.mixin.PopupElement;
19689  * @inheritdoc OO.ui.mixin.TabIndexedElement
19690  * @deprecated Use {@link OO.ui.mixin.TabIndexedElement} instead.
19691  */
19692 OO.ui.TabIndexedElement = OO.ui.mixin.TabIndexedElement;
19695  * @inheritdoc OO.ui.mixin.TitledElement
19696  * @deprecated Use {@link OO.ui.mixin.TitledElement} instead.
19697  */
19698 OO.ui.TitledElement = OO.ui.mixin.TitledElement;
19700 }( OO ) );