]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - resources/lib/oojs-ui/oojs-ui-core.js
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / resources / lib / oojs-ui / oojs-ui-core.js
1 /*!
2  * OOjs UI v0.23.0
3  * https://www.mediawiki.org/wiki/OOjs_UI
4  *
5  * Copyright 2011–2017 OOjs UI Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2017-09-05T21:23:58Z
10  */
11 ( function ( OO ) {
12
13 'use strict';
14
15 /**
16  * Namespace for all classes, static methods and static properties.
17  *
18  * @class
19  * @singleton
20  */
21 OO.ui = {};
22
23 OO.ui.bind = $.proxy;
24
25 /**
26  * @property {Object}
27  */
28 OO.ui.Keys = {
29         UNDEFINED: 0,
30         BACKSPACE: 8,
31         DELETE: 46,
32         LEFT: 37,
33         RIGHT: 39,
34         UP: 38,
35         DOWN: 40,
36         ENTER: 13,
37         END: 35,
38         HOME: 36,
39         TAB: 9,
40         PAGEUP: 33,
41         PAGEDOWN: 34,
42         ESCAPE: 27,
43         SHIFT: 16,
44         SPACE: 32
45 };
46
47 /**
48  * Constants for MouseEvent.which
49  *
50  * @property {Object}
51  */
52 OO.ui.MouseButtons = {
53         LEFT: 1,
54         MIDDLE: 2,
55         RIGHT: 3
56 };
57
58 /**
59  * @property {number}
60  * @private
61  */
62 OO.ui.elementId = 0;
63
64 /**
65  * Generate a unique ID for element
66  *
67  * @return {string} ID
68  */
69 OO.ui.generateElementId = function () {
70         OO.ui.elementId++;
71         return 'oojsui-' + OO.ui.elementId;
72 };
73
74 /**
75  * Check if an element is focusable.
76  * Inspired by :focusable in jQueryUI v1.11.4 - 2015-04-14
77  *
78  * @param {jQuery} $element Element to test
79  * @return {boolean} Element is focusable
80  */
81 OO.ui.isFocusableElement = function ( $element ) {
82         var nodeName,
83                 element = $element[ 0 ];
84
85         // Anything disabled is not focusable
86         if ( element.disabled ) {
87                 return false;
88         }
89
90         // Check if the element is visible
91         if ( !(
92                 // This is quicker than calling $element.is( ':visible' )
93                 $.expr.pseudos.visible( element ) &&
94                 // Check that all parents are visible
95                 !$element.parents().addBack().filter( function () {
96                         return $.css( this, 'visibility' ) === 'hidden';
97                 } ).length
98         ) ) {
99                 return false;
100         }
101
102         // Check if the element is ContentEditable, which is the string 'true'
103         if ( element.contentEditable === 'true' ) {
104                 return true;
105         }
106
107         // Anything with a non-negative numeric tabIndex is focusable.
108         // Use .prop to avoid browser bugs
109         if ( $element.prop( 'tabIndex' ) >= 0 ) {
110                 return true;
111         }
112
113         // Some element types are naturally focusable
114         // (indexOf is much faster than regex in Chrome and about the
115         // same in FF: https://jsperf.com/regex-vs-indexof-array2)
116         nodeName = element.nodeName.toLowerCase();
117         if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName ) !== -1 ) {
118                 return true;
119         }
120
121         // Links and areas are focusable if they have an href
122         if ( ( nodeName === 'a' || nodeName === 'area' ) && $element.attr( 'href' ) !== undefined ) {
123                 return true;
124         }
125
126         return false;
127 };
128
129 /**
130  * Find a focusable child
131  *
132  * @param {jQuery} $container Container to search in
133  * @param {boolean} [backwards] Search backwards
134  * @return {jQuery} Focusable child, or an empty jQuery object if none found
135  */
136 OO.ui.findFocusable = function ( $container, backwards ) {
137         var $focusable = $( [] ),
138                 // $focusableCandidates is a superset of things that
139                 // could get matched by isFocusableElement
140                 $focusableCandidates = $container
141                         .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
142
143         if ( backwards ) {
144                 $focusableCandidates = Array.prototype.reverse.call( $focusableCandidates );
145         }
146
147         $focusableCandidates.each( function () {
148                 var $this = $( this );
149                 if ( OO.ui.isFocusableElement( $this ) ) {
150                         $focusable = $this;
151                         return false;
152                 }
153         } );
154         return $focusable;
155 };
156
157 /**
158  * Get the user's language and any fallback languages.
159  *
160  * These language codes are used to localize user interface elements in the user's language.
161  *
162  * In environments that provide a localization system, this function should be overridden to
163  * return the user's language(s). The default implementation returns English (en) only.
164  *
165  * @return {string[]} Language codes, in descending order of priority
166  */
167 OO.ui.getUserLanguages = function () {
168         return [ 'en' ];
169 };
170
171 /**
172  * Get a value in an object keyed by language code.
173  *
174  * @param {Object.<string,Mixed>} obj Object keyed by language code
175  * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
176  * @param {string} [fallback] Fallback code, used if no matching language can be found
177  * @return {Mixed} Local value
178  */
179 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
180         var i, len, langs;
181
182         // Requested language
183         if ( obj[ lang ] ) {
184                 return obj[ lang ];
185         }
186         // Known user language
187         langs = OO.ui.getUserLanguages();
188         for ( i = 0, len = langs.length; i < len; i++ ) {
189                 lang = langs[ i ];
190                 if ( obj[ lang ] ) {
191                         return obj[ lang ];
192                 }
193         }
194         // Fallback language
195         if ( obj[ fallback ] ) {
196                 return obj[ fallback ];
197         }
198         // First existing language
199         for ( lang in obj ) {
200                 return obj[ lang ];
201         }
202
203         return undefined;
204 };
205
206 /**
207  * Check if a node is contained within another node
208  *
209  * Similar to jQuery#contains except a list of containers can be supplied
210  * and a boolean argument allows you to include the container in the match list
211  *
212  * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
213  * @param {HTMLElement} contained Node to find
214  * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
215  * @return {boolean} The node is in the list of target nodes
216  */
217 OO.ui.contains = function ( containers, contained, matchContainers ) {
218         var i;
219         if ( !Array.isArray( containers ) ) {
220                 containers = [ containers ];
221         }
222         for ( i = containers.length - 1; i >= 0; i-- ) {
223                 if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
224                         return true;
225                 }
226         }
227         return false;
228 };
229
230 /**
231  * Return a function, that, as long as it continues to be invoked, will not
232  * be triggered. The function will be called after it stops being called for
233  * N milliseconds. If `immediate` is passed, trigger the function on the
234  * leading edge, instead of the trailing.
235  *
236  * Ported from: http://underscorejs.org/underscore.js
237  *
238  * @param {Function} func Function to debounce
239  * @param {number} [wait=0] Wait period in milliseconds
240  * @param {boolean} [immediate] Trigger on leading edge
241  * @return {Function} Debounced function
242  */
243 OO.ui.debounce = function ( func, wait, immediate ) {
244         var timeout;
245         return function () {
246                 var context = this,
247                         args = arguments,
248                         later = function () {
249                                 timeout = null;
250                                 if ( !immediate ) {
251                                         func.apply( context, args );
252                                 }
253                         };
254                 if ( immediate && !timeout ) {
255                         func.apply( context, args );
256                 }
257                 if ( !timeout || wait ) {
258                         clearTimeout( timeout );
259                         timeout = setTimeout( later, wait );
260                 }
261         };
262 };
263
264 /**
265  * Puts a console warning with provided message.
266  *
267  * @param {string} message Message
268  */
269 OO.ui.warnDeprecation = function ( message ) {
270         if ( OO.getProp( window, 'console', 'warn' ) !== undefined ) {
271                 // eslint-disable-next-line no-console
272                 console.warn( message );
273         }
274 };
275
276 /**
277  * Returns a function, that, when invoked, will only be triggered at most once
278  * during a given window of time. If called again during that window, it will
279  * wait until the window ends and then trigger itself again.
280  *
281  * As it's not knowable to the caller whether the function will actually run
282  * when the wrapper is called, return values from the function are entirely
283  * discarded.
284  *
285  * @param {Function} func Function to throttle
286  * @param {number} wait Throttle window length, in milliseconds
287  * @return {Function} Throttled function
288  */
289 OO.ui.throttle = function ( func, wait ) {
290         var context, args, timeout,
291                 previous = 0,
292                 run = function () {
293                         timeout = null;
294                         previous = OO.ui.now();
295                         func.apply( context, args );
296                 };
297         return function () {
298                 // Check how long it's been since the last time the function was
299                 // called, and whether it's more or less than the requested throttle
300                 // period. If it's less, run the function immediately. If it's more,
301                 // set a timeout for the remaining time -- but don't replace an
302                 // existing timeout, since that'd indefinitely prolong the wait.
303                 var remaining = wait - ( OO.ui.now() - previous );
304                 context = this;
305                 args = arguments;
306                 if ( remaining <= 0 ) {
307                         // Note: unless wait was ridiculously large, this means we'll
308                         // automatically run the first time the function was called in a
309                         // given period. (If you provide a wait period larger than the
310                         // current Unix timestamp, you *deserve* unexpected behavior.)
311                         clearTimeout( timeout );
312                         run();
313                 } else if ( !timeout ) {
314                         timeout = setTimeout( run, remaining );
315                 }
316         };
317 };
318
319 /**
320  * A (possibly faster) way to get the current timestamp as an integer
321  *
322  * @return {number} Current timestamp, in milliseconds since the Unix epoch
323  */
324 OO.ui.now = Date.now || function () {
325         return new Date().getTime();
326 };
327
328 /**
329  * Reconstitute a JavaScript object corresponding to a widget created by
330  * the PHP implementation.
331  *
332  * This is an alias for `OO.ui.Element.static.infuse()`.
333  *
334  * @param {string|HTMLElement|jQuery} idOrNode
335  *   A DOM id (if a string) or node for the widget to infuse.
336  * @return {OO.ui.Element}
337  *   The `OO.ui.Element` corresponding to this (infusable) document node.
338  */
339 OO.ui.infuse = function ( idOrNode ) {
340         return OO.ui.Element.static.infuse( idOrNode );
341 };
342
343 ( function () {
344         /**
345          * Message store for the default implementation of OO.ui.msg
346          *
347          * Environments that provide a localization system should not use this, but should override
348          * OO.ui.msg altogether.
349          *
350          * @private
351          */
352         var messages = {
353                 // Tool tip for a button that moves items in a list down one place
354                 'ooui-outline-control-move-down': 'Move item down',
355                 // Tool tip for a button that moves items in a list up one place
356                 'ooui-outline-control-move-up': 'Move item up',
357                 // Tool tip for a button that removes items from a list
358                 'ooui-outline-control-remove': 'Remove item',
359                 // Label for the toolbar group that contains a list of all other available tools
360                 'ooui-toolbar-more': 'More',
361                 // Label for the fake tool that expands the full list of tools in a toolbar group
362                 'ooui-toolgroup-expand': 'More',
363                 // Label for the fake tool that collapses the full list of tools in a toolbar group
364                 'ooui-toolgroup-collapse': 'Fewer',
365                 // Default label for the tooltip for the button that removes a tag item
366                 'ooui-item-remove': 'Remove',
367                 // Default label for the accept button of a confirmation dialog
368                 'ooui-dialog-message-accept': 'OK',
369                 // Default label for the reject button of a confirmation dialog
370                 'ooui-dialog-message-reject': 'Cancel',
371                 // Title for process dialog error description
372                 'ooui-dialog-process-error': 'Something went wrong',
373                 // Label for process dialog dismiss error button, visible when describing errors
374                 'ooui-dialog-process-dismiss': 'Dismiss',
375                 // Label for process dialog retry action button, visible when describing only recoverable errors
376                 'ooui-dialog-process-retry': 'Try again',
377                 // Label for process dialog retry action button, visible when describing only warnings
378                 'ooui-dialog-process-continue': 'Continue',
379                 // Label for the file selection widget's select file button
380                 'ooui-selectfile-button-select': 'Select a file',
381                 // Label for the file selection widget if file selection is not supported
382                 'ooui-selectfile-not-supported': 'File selection is not supported',
383                 // Label for the file selection widget when no file is currently selected
384                 'ooui-selectfile-placeholder': 'No file is selected',
385                 // Label for the file selection widget's drop target
386                 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
387         };
388
389         /**
390          * Get a localized message.
391          *
392          * After the message key, message parameters may optionally be passed. In the default implementation,
393          * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
394          * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
395          * they support unnamed, ordered message parameters.
396          *
397          * In environments that provide a localization system, this function should be overridden to
398          * return the message translated in the user's language. The default implementation always returns
399          * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
400          * follows.
401          *
402          *     @example
403          *     var i, iLen, button,
404          *         messagePath = 'oojs-ui/dist/i18n/',
405          *         languages = [ $.i18n().locale, 'ur', 'en' ],
406          *         languageMap = {};
407          *
408          *     for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
409          *         languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
410          *     }
411          *
412          *     $.i18n().load( languageMap ).done( function() {
413          *         // Replace the built-in `msg` only once we've loaded the internationalization.
414          *         // OOjs UI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
415          *         // you put off creating any widgets until this promise is complete, no English
416          *         // will be displayed.
417          *         OO.ui.msg = $.i18n;
418          *
419          *         // A button displaying "OK" in the default locale
420          *         button = new OO.ui.ButtonWidget( {
421          *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
422          *             icon: 'check'
423          *         } );
424          *         $( 'body' ).append( button.$element );
425          *
426          *         // A button displaying "OK" in Urdu
427          *         $.i18n().locale = 'ur';
428          *         button = new OO.ui.ButtonWidget( {
429          *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
430          *             icon: 'check'
431          *         } );
432          *         $( 'body' ).append( button.$element );
433          *     } );
434          *
435          * @param {string} key Message key
436          * @param {...Mixed} [params] Message parameters
437          * @return {string} Translated message with parameters substituted
438          */
439         OO.ui.msg = function ( key ) {
440                 var message = messages[ key ],
441                         params = Array.prototype.slice.call( arguments, 1 );
442                 if ( typeof message === 'string' ) {
443                         // Perform $1 substitution
444                         message = message.replace( /\$(\d+)/g, function ( unused, n ) {
445                                 var i = parseInt( n, 10 );
446                                 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
447                         } );
448                 } else {
449                         // Return placeholder if message not found
450                         message = '[' + key + ']';
451                 }
452                 return message;
453         };
454 }() );
455
456 /**
457  * Package a message and arguments for deferred resolution.
458  *
459  * Use this when you are statically specifying a message and the message may not yet be present.
460  *
461  * @param {string} key Message key
462  * @param {...Mixed} [params] Message parameters
463  * @return {Function} Function that returns the resolved message when executed
464  */
465 OO.ui.deferMsg = function () {
466         var args = arguments;
467         return function () {
468                 return OO.ui.msg.apply( OO.ui, args );
469         };
470 };
471
472 /**
473  * Resolve a message.
474  *
475  * If the message is a function it will be executed, otherwise it will pass through directly.
476  *
477  * @param {Function|string} msg Deferred message, or message text
478  * @return {string} Resolved message
479  */
480 OO.ui.resolveMsg = function ( msg ) {
481         if ( $.isFunction( msg ) ) {
482                 return msg();
483         }
484         return msg;
485 };
486
487 /**
488  * @param {string} url
489  * @return {boolean}
490  */
491 OO.ui.isSafeUrl = function ( url ) {
492         // Keep this function in sync with php/Tag.php
493         var i, protocolWhitelist;
494
495         function stringStartsWith( haystack, needle ) {
496                 return haystack.substr( 0, needle.length ) === needle;
497         }
498
499         protocolWhitelist = [
500                 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
501                 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
502                 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
503         ];
504
505         if ( url === '' ) {
506                 return true;
507         }
508
509         for ( i = 0; i < protocolWhitelist.length; i++ ) {
510                 if ( stringStartsWith( url, protocolWhitelist[ i ] + ':' ) ) {
511                         return true;
512                 }
513         }
514
515         // This matches '//' too
516         if ( stringStartsWith( url, '/' ) || stringStartsWith( url, './' ) ) {
517                 return true;
518         }
519         if ( stringStartsWith( url, '?' ) || stringStartsWith( url, '#' ) ) {
520                 return true;
521         }
522
523         return false;
524 };
525
526 /**
527  * Check if the user has a 'mobile' device.
528  *
529  * For our purposes this means the user is primarily using an
530  * on-screen keyboard, touch input instead of a mouse and may
531  * have a physically small display.
532  *
533  * It is left up to implementors to decide how to compute this
534  * so the default implementation always returns false.
535  *
536  * @return {boolean} Use is on a mobile device
537  */
538 OO.ui.isMobile = function () {
539         return false;
540 };
541
542 /*!
543  * Mixin namespace.
544  */
545
546 /**
547  * Namespace for OOjs UI mixins.
548  *
549  * Mixins are named according to the type of object they are intended to
550  * be mixed in to.  For example, OO.ui.mixin.GroupElement is intended to be
551  * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
552  * is intended to be mixed in to an instance of OO.ui.Widget.
553  *
554  * @class
555  * @singleton
556  */
557 OO.ui.mixin = {};
558
559 /**
560  * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
561  * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
562  * connected to them and can't be interacted with.
563  *
564  * @abstract
565  * @class
566  *
567  * @constructor
568  * @param {Object} [config] Configuration options
569  * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
570  *  to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
571  *  for an example.
572  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
573  * @cfg {string} [id] The HTML id attribute used in the rendered tag.
574  * @cfg {string} [text] Text to insert
575  * @cfg {Array} [content] An array of content elements to append (after #text).
576  *  Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
577  *  Instances of OO.ui.Element will have their $element appended.
578  * @cfg {jQuery} [$content] Content elements to append (after #text).
579  * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
580  * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
581  *  Data can also be specified with the #setData method.
582  */
583 OO.ui.Element = function OoUiElement( config ) {
584         this.initialConfig = config;
585         // Configuration initialization
586         config = config || {};
587
588         // Properties
589         this.$ = $;
590         this.elementId = null;
591         this.visible = true;
592         this.data = config.data;
593         this.$element = config.$element ||
594                 $( document.createElement( this.getTagName() ) );
595         this.elementGroup = null;
596
597         // Initialization
598         if ( Array.isArray( config.classes ) ) {
599                 this.$element.addClass( config.classes.join( ' ' ) );
600         }
601         if ( config.id ) {
602                 this.setElementId( config.id );
603         }
604         if ( config.text ) {
605                 this.$element.text( config.text );
606         }
607         if ( config.content ) {
608                 // The `content` property treats plain strings as text; use an
609                 // HtmlSnippet to append HTML content.  `OO.ui.Element`s get their
610                 // appropriate $element appended.
611                 this.$element.append( config.content.map( function ( v ) {
612                         if ( typeof v === 'string' ) {
613                                 // Escape string so it is properly represented in HTML.
614                                 return document.createTextNode( v );
615                         } else if ( v instanceof OO.ui.HtmlSnippet ) {
616                                 // Bypass escaping.
617                                 return v.toString();
618                         } else if ( v instanceof OO.ui.Element ) {
619                                 return v.$element;
620                         }
621                         return v;
622                 } ) );
623         }
624         if ( config.$content ) {
625                 // The `$content` property treats plain strings as HTML.
626                 this.$element.append( config.$content );
627         }
628 };
629
630 /* Setup */
631
632 OO.initClass( OO.ui.Element );
633
634 /* Static Properties */
635
636 /**
637  * The name of the HTML tag used by the element.
638  *
639  * The static value may be ignored if the #getTagName method is overridden.
640  *
641  * @static
642  * @inheritable
643  * @property {string}
644  */
645 OO.ui.Element.static.tagName = 'div';
646
647 /* Static Methods */
648
649 /**
650  * Reconstitute a JavaScript object corresponding to a widget created
651  * by the PHP implementation.
652  *
653  * @param {string|HTMLElement|jQuery} idOrNode
654  *   A DOM id (if a string) or node for the widget to infuse.
655  * @return {OO.ui.Element}
656  *   The `OO.ui.Element` corresponding to this (infusable) document node.
657  *   For `Tag` objects emitted on the HTML side (used occasionally for content)
658  *   the value returned is a newly-created Element wrapping around the existing
659  *   DOM node.
660  */
661 OO.ui.Element.static.infuse = function ( idOrNode ) {
662         var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
663         // Verify that the type matches up.
664         // FIXME: uncomment after T89721 is fixed, see T90929.
665         /*
666         if ( !( obj instanceof this['class'] ) ) {
667                 throw new Error( 'Infusion type mismatch!' );
668         }
669         */
670         return obj;
671 };
672
673 /**
674  * Implementation helper for `infuse`; skips the type check and has an
675  * extra property so that only the top-level invocation touches the DOM.
676  *
677  * @private
678  * @param {string|HTMLElement|jQuery} idOrNode
679  * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
680  *     when the top-level widget of this infusion is inserted into DOM,
681  *     replacing the original node; or false for top-level invocation.
682  * @return {OO.ui.Element}
683  */
684 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
685         // look for a cached result of a previous infusion.
686         var id, $elem, error, data, cls, parts, parent, obj, top, state, infusedChildren;
687         if ( typeof idOrNode === 'string' ) {
688                 id = idOrNode;
689                 $elem = $( document.getElementById( id ) );
690         } else {
691                 $elem = $( idOrNode );
692                 id = $elem.attr( 'id' );
693         }
694         if ( !$elem.length ) {
695                 if ( typeof idOrNode === 'string' ) {
696                         error = 'Widget not found: ' + idOrNode;
697                 } else if ( idOrNode && idOrNode.selector ) {
698                         error = 'Widget not found: ' + idOrNode.selector;
699                 } else {
700                         error = 'Widget not found';
701                 }
702                 throw new Error( error );
703         }
704         if ( $elem[ 0 ].oouiInfused ) {
705                 $elem = $elem[ 0 ].oouiInfused;
706         }
707         data = $elem.data( 'ooui-infused' );
708         if ( data ) {
709                 // cached!
710                 if ( data === true ) {
711                         throw new Error( 'Circular dependency! ' + id );
712                 }
713                 if ( domPromise ) {
714                         // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
715                         state = data.constructor.static.gatherPreInfuseState( $elem, data );
716                         // restore dynamic state after the new element is re-inserted into DOM under infused parent
717                         domPromise.done( data.restorePreInfuseState.bind( data, state ) );
718                         infusedChildren = $elem.data( 'ooui-infused-children' );
719                         if ( infusedChildren && infusedChildren.length ) {
720                                 infusedChildren.forEach( function ( data ) {
721                                         var state = data.constructor.static.gatherPreInfuseState( $elem, data );
722                                         domPromise.done( data.restorePreInfuseState.bind( data, state ) );
723                                 } );
724                         }
725                 }
726                 return data;
727         }
728         data = $elem.attr( 'data-ooui' );
729         if ( !data ) {
730                 throw new Error( 'No infusion data found: ' + id );
731         }
732         try {
733                 data = JSON.parse( data );
734         } catch ( _ ) {
735                 data = null;
736         }
737         if ( !( data && data._ ) ) {
738                 throw new Error( 'No valid infusion data found: ' + id );
739         }
740         if ( data._ === 'Tag' ) {
741                 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
742                 return new OO.ui.Element( { $element: $elem } );
743         }
744         parts = data._.split( '.' );
745         cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
746         if ( cls === undefined ) {
747                 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
748         }
749
750         // Verify that we're creating an OO.ui.Element instance
751         parent = cls.parent;
752
753         while ( parent !== undefined ) {
754                 if ( parent === OO.ui.Element ) {
755                         // Safe
756                         break;
757                 }
758
759                 parent = parent.parent;
760         }
761
762         if ( parent !== OO.ui.Element ) {
763                 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
764         }
765
766         if ( domPromise === false ) {
767                 top = $.Deferred();
768                 domPromise = top.promise();
769         }
770         $elem.data( 'ooui-infused', true ); // prevent loops
771         data.id = id; // implicit
772         infusedChildren = [];
773         data = OO.copy( data, null, function deserialize( value ) {
774                 var infused;
775                 if ( OO.isPlainObject( value ) ) {
776                         if ( value.tag ) {
777                                 infused = OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
778                                 infusedChildren.push( infused );
779                                 // Flatten the structure
780                                 infusedChildren.push.apply( infusedChildren, infused.$element.data( 'ooui-infused-children' ) || [] );
781                                 infused.$element.removeData( 'ooui-infused-children' );
782                                 return infused;
783                         }
784                         if ( value.html !== undefined ) {
785                                 return new OO.ui.HtmlSnippet( value.html );
786                         }
787                 }
788         } );
789         // allow widgets to reuse parts of the DOM
790         data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data );
791         // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
792         state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
793         // rebuild widget
794         // eslint-disable-next-line new-cap
795         obj = new cls( data );
796         // now replace old DOM with this new DOM.
797         if ( top ) {
798                 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
799                 // so only mutate the DOM if we need to.
800                 if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
801                         $elem.replaceWith( obj.$element );
802                         // This element is now gone from the DOM, but if anyone is holding a reference to it,
803                         // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
804                         // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
805                         $elem[ 0 ].oouiInfused = obj.$element;
806                 }
807                 top.resolve();
808         }
809         obj.$element.data( 'ooui-infused', obj );
810         obj.$element.data( 'ooui-infused-children', infusedChildren );
811         // set the 'data-ooui' attribute so we can identify infused widgets
812         obj.$element.attr( 'data-ooui', '' );
813         // restore dynamic state after the new element is inserted into DOM
814         domPromise.done( obj.restorePreInfuseState.bind( obj, state ) );
815         return obj;
816 };
817
818 /**
819  * Pick out parts of `node`'s DOM to be reused when infusing a widget.
820  *
821  * This method **must not** make any changes to the DOM, only find interesting pieces and add them
822  * to `config` (which should then be returned). Actual DOM juggling should then be done by the
823  * constructor, which will be given the enhanced config.
824  *
825  * @protected
826  * @param {HTMLElement} node
827  * @param {Object} config
828  * @return {Object}
829  */
830 OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) {
831         return config;
832 };
833
834 /**
835  * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
836  * (and its children) that represent an Element of the same class and the given configuration,
837  * generated by the PHP implementation.
838  *
839  * This method is called just before `node` is detached from the DOM. The return value of this
840  * function will be passed to #restorePreInfuseState after the newly created widget's #$element
841  * is inserted into DOM to replace `node`.
842  *
843  * @protected
844  * @param {HTMLElement} node
845  * @param {Object} config
846  * @return {Object}
847  */
848 OO.ui.Element.static.gatherPreInfuseState = function () {
849         return {};
850 };
851
852 /**
853  * Get a jQuery function within a specific document.
854  *
855  * @static
856  * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
857  * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
858  *   not in an iframe
859  * @return {Function} Bound jQuery function
860  */
861 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
862         function wrapper( selector ) {
863                 return $( selector, wrapper.context );
864         }
865
866         wrapper.context = this.getDocument( context );
867
868         if ( $iframe ) {
869                 wrapper.$iframe = $iframe;
870         }
871
872         return wrapper;
873 };
874
875 /**
876  * Get the document of an element.
877  *
878  * @static
879  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
880  * @return {HTMLDocument|null} Document object
881  */
882 OO.ui.Element.static.getDocument = function ( obj ) {
883         // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
884         return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
885                 // Empty jQuery selections might have a context
886                 obj.context ||
887                 // HTMLElement
888                 obj.ownerDocument ||
889                 // Window
890                 obj.document ||
891                 // HTMLDocument
892                 ( obj.nodeType === Node.DOCUMENT_NODE && obj ) ||
893                 null;
894 };
895
896 /**
897  * Get the window of an element or document.
898  *
899  * @static
900  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
901  * @return {Window} Window object
902  */
903 OO.ui.Element.static.getWindow = function ( obj ) {
904         var doc = this.getDocument( obj );
905         return doc.defaultView;
906 };
907
908 /**
909  * Get the direction of an element or document.
910  *
911  * @static
912  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
913  * @return {string} Text direction, either 'ltr' or 'rtl'
914  */
915 OO.ui.Element.static.getDir = function ( obj ) {
916         var isDoc, isWin;
917
918         if ( obj instanceof jQuery ) {
919                 obj = obj[ 0 ];
920         }
921         isDoc = obj.nodeType === Node.DOCUMENT_NODE;
922         isWin = obj.document !== undefined;
923         if ( isDoc || isWin ) {
924                 if ( isWin ) {
925                         obj = obj.document;
926                 }
927                 obj = obj.body;
928         }
929         return $( obj ).css( 'direction' );
930 };
931
932 /**
933  * Get the offset between two frames.
934  *
935  * TODO: Make this function not use recursion.
936  *
937  * @static
938  * @param {Window} from Window of the child frame
939  * @param {Window} [to=window] Window of the parent frame
940  * @param {Object} [offset] Offset to start with, used internally
941  * @return {Object} Offset object, containing left and top properties
942  */
943 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
944         var i, len, frames, frame, rect;
945
946         if ( !to ) {
947                 to = window;
948         }
949         if ( !offset ) {
950                 offset = { top: 0, left: 0 };
951         }
952         if ( from.parent === from ) {
953                 return offset;
954         }
955
956         // Get iframe element
957         frames = from.parent.document.getElementsByTagName( 'iframe' );
958         for ( i = 0, len = frames.length; i < len; i++ ) {
959                 if ( frames[ i ].contentWindow === from ) {
960                         frame = frames[ i ];
961                         break;
962                 }
963         }
964
965         // Recursively accumulate offset values
966         if ( frame ) {
967                 rect = frame.getBoundingClientRect();
968                 offset.left += rect.left;
969                 offset.top += rect.top;
970                 if ( from !== to ) {
971                         this.getFrameOffset( from.parent, offset );
972                 }
973         }
974         return offset;
975 };
976
977 /**
978  * Get the offset between two elements.
979  *
980  * The two elements may be in a different frame, but in that case the frame $element is in must
981  * be contained in the frame $anchor is in.
982  *
983  * @static
984  * @param {jQuery} $element Element whose position to get
985  * @param {jQuery} $anchor Element to get $element's position relative to
986  * @return {Object} Translated position coordinates, containing top and left properties
987  */
988 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
989         var iframe, iframePos,
990                 pos = $element.offset(),
991                 anchorPos = $anchor.offset(),
992                 elementDocument = this.getDocument( $element ),
993                 anchorDocument = this.getDocument( $anchor );
994
995         // If $element isn't in the same document as $anchor, traverse up
996         while ( elementDocument !== anchorDocument ) {
997                 iframe = elementDocument.defaultView.frameElement;
998                 if ( !iframe ) {
999                         throw new Error( '$element frame is not contained in $anchor frame' );
1000                 }
1001                 iframePos = $( iframe ).offset();
1002                 pos.left += iframePos.left;
1003                 pos.top += iframePos.top;
1004                 elementDocument = iframe.ownerDocument;
1005         }
1006         pos.left -= anchorPos.left;
1007         pos.top -= anchorPos.top;
1008         return pos;
1009 };
1010
1011 /**
1012  * Get element border sizes.
1013  *
1014  * @static
1015  * @param {HTMLElement} el Element to measure
1016  * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1017  */
1018 OO.ui.Element.static.getBorders = function ( el ) {
1019         var doc = el.ownerDocument,
1020                 win = doc.defaultView,
1021                 style = win.getComputedStyle( el, null ),
1022                 $el = $( el ),
1023                 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1024                 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1025                 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1026                 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1027
1028         return {
1029                 top: top,
1030                 left: left,
1031                 bottom: bottom,
1032                 right: right
1033         };
1034 };
1035
1036 /**
1037  * Get dimensions of an element or window.
1038  *
1039  * @static
1040  * @param {HTMLElement|Window} el Element to measure
1041  * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1042  */
1043 OO.ui.Element.static.getDimensions = function ( el ) {
1044         var $el, $win,
1045                 doc = el.ownerDocument || el.document,
1046                 win = doc.defaultView;
1047
1048         if ( win === el || el === doc.documentElement ) {
1049                 $win = $( win );
1050                 return {
1051                         borders: { top: 0, left: 0, bottom: 0, right: 0 },
1052                         scroll: {
1053                                 top: $win.scrollTop(),
1054                                 left: $win.scrollLeft()
1055                         },
1056                         scrollbar: { right: 0, bottom: 0 },
1057                         rect: {
1058                                 top: 0,
1059                                 left: 0,
1060                                 bottom: $win.innerHeight(),
1061                                 right: $win.innerWidth()
1062                         }
1063                 };
1064         } else {
1065                 $el = $( el );
1066                 return {
1067                         borders: this.getBorders( el ),
1068                         scroll: {
1069                                 top: $el.scrollTop(),
1070                                 left: $el.scrollLeft()
1071                         },
1072                         scrollbar: {
1073                                 right: $el.innerWidth() - el.clientWidth,
1074                                 bottom: $el.innerHeight() - el.clientHeight
1075                         },
1076                         rect: el.getBoundingClientRect()
1077                 };
1078         }
1079 };
1080
1081 /**
1082  * Get the number of pixels that an element's content is scrolled to the left.
1083  *
1084  * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1085  * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1086  *
1087  * This function smooths out browser inconsistencies (nicely described in the README at
1088  * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1089  * with Firefox's 'scrollLeft', which seems the sanest.
1090  *
1091  * @static
1092  * @method
1093  * @param {HTMLElement|Window} el Element to measure
1094  * @return {number} Scroll position from the left.
1095  *  If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
1096  *  and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1097  *  If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
1098  *  and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1099  */
1100 OO.ui.Element.static.getScrollLeft = ( function () {
1101         var rtlScrollType = null;
1102
1103         function test() {
1104                 var $definer = $( '<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>' ),
1105                         definer = $definer[ 0 ];
1106
1107                 $definer.appendTo( 'body' );
1108                 if ( definer.scrollLeft > 0 ) {
1109                         // Safari, Chrome
1110                         rtlScrollType = 'default';
1111                 } else {
1112                         definer.scrollLeft = 1;
1113                         if ( definer.scrollLeft === 0 ) {
1114                                 // Firefox, old Opera
1115                                 rtlScrollType = 'negative';
1116                         } else {
1117                                 // Internet Explorer, Edge
1118                                 rtlScrollType = 'reverse';
1119                         }
1120                 }
1121                 $definer.remove();
1122         }
1123
1124         return function getScrollLeft( el ) {
1125                 var isRoot = el.window === el ||
1126                                 el === el.ownerDocument.body ||
1127                                 el === el.ownerDocument.documentElement,
1128                         scrollLeft = isRoot ? $( window ).scrollLeft() : el.scrollLeft,
1129                         // All browsers use the correct scroll type ('negative') on the root, so don't
1130                         // do any fixups when looking at the root element
1131                         direction = isRoot ? 'ltr' : $( el ).css( 'direction' );
1132
1133                 if ( direction === 'rtl' ) {
1134                         if ( rtlScrollType === null ) {
1135                                 test();
1136                         }
1137                         if ( rtlScrollType === 'reverse' ) {
1138                                 scrollLeft = -scrollLeft;
1139                         } else if ( rtlScrollType === 'default' ) {
1140                                 scrollLeft = scrollLeft - el.scrollWidth + el.clientWidth;
1141                         }
1142                 }
1143
1144                 return scrollLeft;
1145         };
1146 }() );
1147
1148 /**
1149  * Get the root scrollable element of given element's document.
1150  *
1151  * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1152  * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1153  * lets us use 'body' or 'documentElement' based on what is working.
1154  *
1155  * https://code.google.com/p/chromium/issues/detail?id=303131
1156  *
1157  * @static
1158  * @param {HTMLElement} el Element to find root scrollable parent for
1159  * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1160  *     depending on browser
1161  */
1162 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1163         var scrollTop, body;
1164
1165         if ( OO.ui.scrollableElement === undefined ) {
1166                 body = el.ownerDocument.body;
1167                 scrollTop = body.scrollTop;
1168                 body.scrollTop = 1;
1169
1170                 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1171                 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1172                 if ( Math.round( body.scrollTop ) === 1 ) {
1173                         body.scrollTop = scrollTop;
1174                         OO.ui.scrollableElement = 'body';
1175                 } else {
1176                         OO.ui.scrollableElement = 'documentElement';
1177                 }
1178         }
1179
1180         return el.ownerDocument[ OO.ui.scrollableElement ];
1181 };
1182
1183 /**
1184  * Get closest scrollable container.
1185  *
1186  * Traverses up until either a scrollable element or the root is reached, in which case the root
1187  * scrollable element will be returned (see #getRootScrollableElement).
1188  *
1189  * @static
1190  * @param {HTMLElement} el Element to find scrollable container for
1191  * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1192  * @return {HTMLElement} Closest scrollable container
1193  */
1194 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1195         var i, val,
1196                 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1197                 // 'overflow-y' have different values, so we need to check the separate properties.
1198                 props = [ 'overflow-x', 'overflow-y' ],
1199                 $parent = $( el ).parent();
1200
1201         if ( dimension === 'x' || dimension === 'y' ) {
1202                 props = [ 'overflow-' + dimension ];
1203         }
1204
1205         // Special case for the document root (which doesn't really have any scrollable container, since
1206         // it is the ultimate scrollable container, but this is probably saner than null or exception)
1207         if ( $( el ).is( 'html, body' ) ) {
1208                 return this.getRootScrollableElement( el );
1209         }
1210
1211         while ( $parent.length ) {
1212                 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1213                         return $parent[ 0 ];
1214                 }
1215                 i = props.length;
1216                 while ( i-- ) {
1217                         val = $parent.css( props[ i ] );
1218                         // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
1219                         // scrolled in that direction, but they can actually be scrolled programatically. The user can
1220                         // unintentionally perform a scroll in such case even if the application doesn't scroll
1221                         // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
1222                         // This could cause funny issues...
1223                         if ( val === 'auto' || val === 'scroll' ) {
1224                                 return $parent[ 0 ];
1225                         }
1226                 }
1227                 $parent = $parent.parent();
1228         }
1229         // The element is unattached... return something mostly sane
1230         return this.getRootScrollableElement( el );
1231 };
1232
1233 /**
1234  * Scroll element into view.
1235  *
1236  * @static
1237  * @param {HTMLElement} el Element to scroll into view
1238  * @param {Object} [config] Configuration options
1239  * @param {string} [config.duration='fast'] jQuery animation duration value
1240  * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1241  *  to scroll in both directions
1242  * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1243  */
1244 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1245         var position, animations, container, $container, elementDimensions, containerDimensions, $window,
1246                 deferred = $.Deferred();
1247
1248         // Configuration initialization
1249         config = config || {};
1250
1251         animations = {};
1252         container = this.getClosestScrollableContainer( el, config.direction );
1253         $container = $( container );
1254         elementDimensions = this.getDimensions( el );
1255         containerDimensions = this.getDimensions( container );
1256         $window = $( this.getWindow( el ) );
1257
1258         // Compute the element's position relative to the container
1259         if ( $container.is( 'html, body' ) ) {
1260                 // If the scrollable container is the root, this is easy
1261                 position = {
1262                         top: elementDimensions.rect.top,
1263                         bottom: $window.innerHeight() - elementDimensions.rect.bottom,
1264                         left: elementDimensions.rect.left,
1265                         right: $window.innerWidth() - elementDimensions.rect.right
1266                 };
1267         } else {
1268                 // Otherwise, we have to subtract el's coordinates from container's coordinates
1269                 position = {
1270                         top: elementDimensions.rect.top - ( containerDimensions.rect.top + containerDimensions.borders.top ),
1271                         bottom: containerDimensions.rect.bottom - containerDimensions.borders.bottom - containerDimensions.scrollbar.bottom - elementDimensions.rect.bottom,
1272                         left: elementDimensions.rect.left - ( containerDimensions.rect.left + containerDimensions.borders.left ),
1273                         right: containerDimensions.rect.right - containerDimensions.borders.right - containerDimensions.scrollbar.right - elementDimensions.rect.right
1274                 };
1275         }
1276
1277         if ( !config.direction || config.direction === 'y' ) {
1278                 if ( position.top < 0 ) {
1279                         animations.scrollTop = containerDimensions.scroll.top + position.top;
1280                 } else if ( position.top > 0 && position.bottom < 0 ) {
1281                         animations.scrollTop = containerDimensions.scroll.top + Math.min( position.top, -position.bottom );
1282                 }
1283         }
1284         if ( !config.direction || config.direction === 'x' ) {
1285                 if ( position.left < 0 ) {
1286                         animations.scrollLeft = containerDimensions.scroll.left + position.left;
1287                 } else if ( position.left > 0 && position.right < 0 ) {
1288                         animations.scrollLeft = containerDimensions.scroll.left + Math.min( position.left, -position.right );
1289                 }
1290         }
1291         if ( !$.isEmptyObject( animations ) ) {
1292                 $container.stop( true ).animate( animations, config.duration === undefined ? 'fast' : config.duration );
1293                 $container.queue( function ( next ) {
1294                         deferred.resolve();
1295                         next();
1296                 } );
1297         } else {
1298                 deferred.resolve();
1299         }
1300         return deferred.promise();
1301 };
1302
1303 /**
1304  * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1305  * and reserve space for them, because it probably doesn't.
1306  *
1307  * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1308  * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1309  * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1310  * and then reattach (or show) them back.
1311  *
1312  * @static
1313  * @param {HTMLElement} el Element to reconsider the scrollbars on
1314  */
1315 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1316         var i, len, scrollLeft, scrollTop, nodes = [];
1317         // Save scroll position
1318         scrollLeft = el.scrollLeft;
1319         scrollTop = el.scrollTop;
1320         // Detach all children
1321         while ( el.firstChild ) {
1322                 nodes.push( el.firstChild );
1323                 el.removeChild( el.firstChild );
1324         }
1325         // Force reflow
1326         void el.offsetHeight;
1327         // Reattach all children
1328         for ( i = 0, len = nodes.length; i < len; i++ ) {
1329                 el.appendChild( nodes[ i ] );
1330         }
1331         // Restore scroll position (no-op if scrollbars disappeared)
1332         el.scrollLeft = scrollLeft;
1333         el.scrollTop = scrollTop;
1334 };
1335
1336 /* Methods */
1337
1338 /**
1339  * Toggle visibility of an element.
1340  *
1341  * @param {boolean} [show] Make element visible, omit to toggle visibility
1342  * @fires visible
1343  * @chainable
1344  */
1345 OO.ui.Element.prototype.toggle = function ( show ) {
1346         show = show === undefined ? !this.visible : !!show;
1347
1348         if ( show !== this.isVisible() ) {
1349                 this.visible = show;
1350                 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1351                 this.emit( 'toggle', show );
1352         }
1353
1354         return this;
1355 };
1356
1357 /**
1358  * Check if element is visible.
1359  *
1360  * @return {boolean} element is visible
1361  */
1362 OO.ui.Element.prototype.isVisible = function () {
1363         return this.visible;
1364 };
1365
1366 /**
1367  * Get element data.
1368  *
1369  * @return {Mixed} Element data
1370  */
1371 OO.ui.Element.prototype.getData = function () {
1372         return this.data;
1373 };
1374
1375 /**
1376  * Set element data.
1377  *
1378  * @param {Mixed} data Element data
1379  * @chainable
1380  */
1381 OO.ui.Element.prototype.setData = function ( data ) {
1382         this.data = data;
1383         return this;
1384 };
1385
1386 /**
1387  * Set the element has an 'id' attribute.
1388  *
1389  * @param {string} id
1390  * @chainable
1391  */
1392 OO.ui.Element.prototype.setElementId = function ( id ) {
1393         this.elementId = id;
1394         this.$element.attr( 'id', id );
1395         return this;
1396 };
1397
1398 /**
1399  * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1400  * and return its value.
1401  *
1402  * @return {string}
1403  */
1404 OO.ui.Element.prototype.getElementId = function () {
1405         if ( this.elementId === null ) {
1406                 this.setElementId( OO.ui.generateElementId() );
1407         }
1408         return this.elementId;
1409 };
1410
1411 /**
1412  * Check if element supports one or more methods.
1413  *
1414  * @param {string|string[]} methods Method or list of methods to check
1415  * @return {boolean} All methods are supported
1416  */
1417 OO.ui.Element.prototype.supports = function ( methods ) {
1418         var i, len,
1419                 support = 0;
1420
1421         methods = Array.isArray( methods ) ? methods : [ methods ];
1422         for ( i = 0, len = methods.length; i < len; i++ ) {
1423                 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1424                         support++;
1425                 }
1426         }
1427
1428         return methods.length === support;
1429 };
1430
1431 /**
1432  * Update the theme-provided classes.
1433  *
1434  * @localdoc This is called in element mixins and widget classes any time state changes.
1435  *   Updating is debounced, minimizing overhead of changing multiple attributes and
1436  *   guaranteeing that theme updates do not occur within an element's constructor
1437  */
1438 OO.ui.Element.prototype.updateThemeClasses = function () {
1439         OO.ui.theme.queueUpdateElementClasses( this );
1440 };
1441
1442 /**
1443  * Get the HTML tag name.
1444  *
1445  * Override this method to base the result on instance information.
1446  *
1447  * @return {string} HTML tag name
1448  */
1449 OO.ui.Element.prototype.getTagName = function () {
1450         return this.constructor.static.tagName;
1451 };
1452
1453 /**
1454  * Check if the element is attached to the DOM
1455  *
1456  * @return {boolean} The element is attached to the DOM
1457  */
1458 OO.ui.Element.prototype.isElementAttached = function () {
1459         return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1460 };
1461
1462 /**
1463  * Get the DOM document.
1464  *
1465  * @return {HTMLDocument} Document object
1466  */
1467 OO.ui.Element.prototype.getElementDocument = function () {
1468         // Don't cache this in other ways either because subclasses could can change this.$element
1469         return OO.ui.Element.static.getDocument( this.$element );
1470 };
1471
1472 /**
1473  * Get the DOM window.
1474  *
1475  * @return {Window} Window object
1476  */
1477 OO.ui.Element.prototype.getElementWindow = function () {
1478         return OO.ui.Element.static.getWindow( this.$element );
1479 };
1480
1481 /**
1482  * Get closest scrollable container.
1483  *
1484  * @return {HTMLElement} Closest scrollable container
1485  */
1486 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1487         return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1488 };
1489
1490 /**
1491  * Get group element is in.
1492  *
1493  * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1494  */
1495 OO.ui.Element.prototype.getElementGroup = function () {
1496         return this.elementGroup;
1497 };
1498
1499 /**
1500  * Set group element is in.
1501  *
1502  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1503  * @chainable
1504  */
1505 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1506         this.elementGroup = group;
1507         return this;
1508 };
1509
1510 /**
1511  * Scroll element into view.
1512  *
1513  * @param {Object} [config] Configuration options
1514  * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1515  */
1516 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1517         if (
1518                 !this.isElementAttached() ||
1519                 !this.isVisible() ||
1520                 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1521         ) {
1522                 return $.Deferred().resolve();
1523         }
1524         return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1525 };
1526
1527 /**
1528  * Restore the pre-infusion dynamic state for this widget.
1529  *
1530  * This method is called after #$element has been inserted into DOM. The parameter is the return
1531  * value of #gatherPreInfuseState.
1532  *
1533  * @protected
1534  * @param {Object} state
1535  */
1536 OO.ui.Element.prototype.restorePreInfuseState = function () {
1537 };
1538
1539 /**
1540  * Wraps an HTML snippet for use with configuration values which default
1541  * to strings.  This bypasses the default html-escaping done to string
1542  * values.
1543  *
1544  * @class
1545  *
1546  * @constructor
1547  * @param {string} [content] HTML content
1548  */
1549 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
1550         // Properties
1551         this.content = content;
1552 };
1553
1554 /* Setup */
1555
1556 OO.initClass( OO.ui.HtmlSnippet );
1557
1558 /* Methods */
1559
1560 /**
1561  * Render into HTML.
1562  *
1563  * @return {string} Unchanged HTML snippet.
1564  */
1565 OO.ui.HtmlSnippet.prototype.toString = function () {
1566         return this.content;
1567 };
1568
1569 /**
1570  * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1571  * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1572  * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1573  * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1574  * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1575  *
1576  * @abstract
1577  * @class
1578  * @extends OO.ui.Element
1579  * @mixins OO.EventEmitter
1580  *
1581  * @constructor
1582  * @param {Object} [config] Configuration options
1583  */
1584 OO.ui.Layout = function OoUiLayout( config ) {
1585         // Configuration initialization
1586         config = config || {};
1587
1588         // Parent constructor
1589         OO.ui.Layout.parent.call( this, config );
1590
1591         // Mixin constructors
1592         OO.EventEmitter.call( this );
1593
1594         // Initialization
1595         this.$element.addClass( 'oo-ui-layout' );
1596 };
1597
1598 /* Setup */
1599
1600 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1601 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1602
1603 /**
1604  * Widgets are compositions of one or more OOjs UI elements that users can both view
1605  * and interact with. All widgets can be configured and modified via a standard API,
1606  * and their state can change dynamically according to a model.
1607  *
1608  * @abstract
1609  * @class
1610  * @extends OO.ui.Element
1611  * @mixins OO.EventEmitter
1612  *
1613  * @constructor
1614  * @param {Object} [config] Configuration options
1615  * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1616  *  appearance reflects this state.
1617  */
1618 OO.ui.Widget = function OoUiWidget( config ) {
1619         // Initialize config
1620         config = $.extend( { disabled: false }, config );
1621
1622         // Parent constructor
1623         OO.ui.Widget.parent.call( this, config );
1624
1625         // Mixin constructors
1626         OO.EventEmitter.call( this );
1627
1628         // Properties
1629         this.disabled = null;
1630         this.wasDisabled = null;
1631
1632         // Initialization
1633         this.$element.addClass( 'oo-ui-widget' );
1634         this.setDisabled( !!config.disabled );
1635 };
1636
1637 /* Setup */
1638
1639 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1640 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1641
1642 /* Events */
1643
1644 /**
1645  * @event disable
1646  *
1647  * A 'disable' event is emitted when the disabled state of the widget changes
1648  * (i.e. on disable **and** enable).
1649  *
1650  * @param {boolean} disabled Widget is disabled
1651  */
1652
1653 /**
1654  * @event toggle
1655  *
1656  * A 'toggle' event is emitted when the visibility of the widget changes.
1657  *
1658  * @param {boolean} visible Widget is visible
1659  */
1660
1661 /* Methods */
1662
1663 /**
1664  * Check if the widget is disabled.
1665  *
1666  * @return {boolean} Widget is disabled
1667  */
1668 OO.ui.Widget.prototype.isDisabled = function () {
1669         return this.disabled;
1670 };
1671
1672 /**
1673  * Set the 'disabled' state of the widget.
1674  *
1675  * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1676  *
1677  * @param {boolean} disabled Disable widget
1678  * @chainable
1679  */
1680 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1681         var isDisabled;
1682
1683         this.disabled = !!disabled;
1684         isDisabled = this.isDisabled();
1685         if ( isDisabled !== this.wasDisabled ) {
1686                 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1687                 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1688                 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1689                 this.emit( 'disable', isDisabled );
1690                 this.updateThemeClasses();
1691         }
1692         this.wasDisabled = isDisabled;
1693
1694         return this;
1695 };
1696
1697 /**
1698  * Update the disabled state, in case of changes in parent widget.
1699  *
1700  * @chainable
1701  */
1702 OO.ui.Widget.prototype.updateDisabled = function () {
1703         this.setDisabled( this.disabled );
1704         return this;
1705 };
1706
1707 /**
1708  * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1709  * value.
1710  *
1711  * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1712  * instead.
1713  *
1714  * @return {string|null} The ID of the labelable element
1715  */
1716 OO.ui.Widget.prototype.getInputId = function () {
1717         return null;
1718 };
1719
1720 /**
1721  * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1722  * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1723  * override this method to provide intuitive, accessible behavior.
1724  *
1725  * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1726  * Individual widgets may override it too.
1727  *
1728  * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1729  * directly.
1730  */
1731 OO.ui.Widget.prototype.simulateLabelClick = function () {
1732 };
1733
1734 /**
1735  * Theme logic.
1736  *
1737  * @abstract
1738  * @class
1739  *
1740  * @constructor
1741  */
1742 OO.ui.Theme = function OoUiTheme() {
1743         this.elementClassesQueue = [];
1744         this.debouncedUpdateQueuedElementClasses = OO.ui.debounce( this.updateQueuedElementClasses );
1745 };
1746
1747 /* Setup */
1748
1749 OO.initClass( OO.ui.Theme );
1750
1751 /* Methods */
1752
1753 /**
1754  * Get a list of classes to be applied to a widget.
1755  *
1756  * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1757  * otherwise state transitions will not work properly.
1758  *
1759  * @param {OO.ui.Element} element Element for which to get classes
1760  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1761  */
1762 OO.ui.Theme.prototype.getElementClasses = function () {
1763         return { on: [], off: [] };
1764 };
1765
1766 /**
1767  * Update CSS classes provided by the theme.
1768  *
1769  * For elements with theme logic hooks, this should be called any time there's a state change.
1770  *
1771  * @param {OO.ui.Element} element Element for which to update classes
1772  */
1773 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
1774         var $elements = $( [] ),
1775                 classes = this.getElementClasses( element );
1776
1777         if ( element.$icon ) {
1778                 $elements = $elements.add( element.$icon );
1779         }
1780         if ( element.$indicator ) {
1781                 $elements = $elements.add( element.$indicator );
1782         }
1783
1784         $elements
1785                 .removeClass( classes.off.join( ' ' ) )
1786                 .addClass( classes.on.join( ' ' ) );
1787 };
1788
1789 /**
1790  * @private
1791  */
1792 OO.ui.Theme.prototype.updateQueuedElementClasses = function () {
1793         var i;
1794         for ( i = 0; i < this.elementClassesQueue.length; i++ ) {
1795                 this.updateElementClasses( this.elementClassesQueue[ i ] );
1796         }
1797         // Clear the queue
1798         this.elementClassesQueue = [];
1799 };
1800
1801 /**
1802  * Queue #updateElementClasses to be called for this element.
1803  *
1804  * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1805  *   to make them synchronous.
1806  *
1807  * @param {OO.ui.Element} element Element for which to update classes
1808  */
1809 OO.ui.Theme.prototype.queueUpdateElementClasses = function ( element ) {
1810         // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1811         // the most common case (this method is often called repeatedly for the same element).
1812         if ( this.elementClassesQueue.lastIndexOf( element ) !== -1 ) {
1813                 return;
1814         }
1815         this.elementClassesQueue.push( element );
1816         this.debouncedUpdateQueuedElementClasses();
1817 };
1818
1819 /**
1820  * Get the transition duration in milliseconds for dialogs opening/closing
1821  *
1822  * The dialog should be fully rendered this many milliseconds after the
1823  * ready process has executed.
1824  *
1825  * @return {number} Transition duration in milliseconds
1826  */
1827 OO.ui.Theme.prototype.getDialogTransitionDuration = function () {
1828         return 0;
1829 };
1830
1831 /**
1832  * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1833  * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1834  * order in which users will navigate through the focusable elements via the "tab" key.
1835  *
1836  *     @example
1837  *     // TabIndexedElement is mixed into the ButtonWidget class
1838  *     // to provide a tabIndex property.
1839  *     var button1 = new OO.ui.ButtonWidget( {
1840  *         label: 'fourth',
1841  *         tabIndex: 4
1842  *     } );
1843  *     var button2 = new OO.ui.ButtonWidget( {
1844  *         label: 'second',
1845  *         tabIndex: 2
1846  *     } );
1847  *     var button3 = new OO.ui.ButtonWidget( {
1848  *         label: 'third',
1849  *         tabIndex: 3
1850  *     } );
1851  *     var button4 = new OO.ui.ButtonWidget( {
1852  *         label: 'first',
1853  *         tabIndex: 1
1854  *     } );
1855  *     $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1856  *
1857  * @abstract
1858  * @class
1859  *
1860  * @constructor
1861  * @param {Object} [config] Configuration options
1862  * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1863  *  the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1864  *  functionality will be applied to it instead.
1865  * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1866  *  order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1867  *  to remove the element from the tab-navigation flow.
1868  */
1869 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
1870         // Configuration initialization
1871         config = $.extend( { tabIndex: 0 }, config );
1872
1873         // Properties
1874         this.$tabIndexed = null;
1875         this.tabIndex = null;
1876
1877         // Events
1878         this.connect( this, { disable: 'onTabIndexedElementDisable' } );
1879
1880         // Initialization
1881         this.setTabIndex( config.tabIndex );
1882         this.setTabIndexedElement( config.$tabIndexed || this.$element );
1883 };
1884
1885 /* Setup */
1886
1887 OO.initClass( OO.ui.mixin.TabIndexedElement );
1888
1889 /* Methods */
1890
1891 /**
1892  * Set the element that should use the tabindex functionality.
1893  *
1894  * This method is used to retarget a tabindex mixin so that its functionality applies
1895  * to the specified element. If an element is currently using the functionality, the mixin’s
1896  * effect on that element is removed before the new element is set up.
1897  *
1898  * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1899  * @chainable
1900  */
1901 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
1902         var tabIndex = this.tabIndex;
1903         // Remove attributes from old $tabIndexed
1904         this.setTabIndex( null );
1905         // Force update of new $tabIndexed
1906         this.$tabIndexed = $tabIndexed;
1907         this.tabIndex = tabIndex;
1908         return this.updateTabIndex();
1909 };
1910
1911 /**
1912  * Set the value of the tabindex.
1913  *
1914  * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1915  * @chainable
1916  */
1917 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
1918         tabIndex = /^-?\d+$/.test( tabIndex ) ? Number( tabIndex ) : null;
1919
1920         if ( this.tabIndex !== tabIndex ) {
1921                 this.tabIndex = tabIndex;
1922                 this.updateTabIndex();
1923         }
1924
1925         return this;
1926 };
1927
1928 /**
1929  * Update the `tabindex` attribute, in case of changes to tab index or
1930  * disabled state.
1931  *
1932  * @private
1933  * @chainable
1934  */
1935 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
1936         if ( this.$tabIndexed ) {
1937                 if ( this.tabIndex !== null ) {
1938                         // Do not index over disabled elements
1939                         this.$tabIndexed.attr( {
1940                                 tabindex: this.isDisabled() ? -1 : this.tabIndex,
1941                                 // Support: ChromeVox and NVDA
1942                                 // These do not seem to inherit aria-disabled from parent elements
1943                                 'aria-disabled': this.isDisabled().toString()
1944                         } );
1945                 } else {
1946                         this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
1947                 }
1948         }
1949         return this;
1950 };
1951
1952 /**
1953  * Handle disable events.
1954  *
1955  * @private
1956  * @param {boolean} disabled Element is disabled
1957  */
1958 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
1959         this.updateTabIndex();
1960 };
1961
1962 /**
1963  * Get the value of the tabindex.
1964  *
1965  * @return {number|null} Tabindex value
1966  */
1967 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
1968         return this.tabIndex;
1969 };
1970
1971 /**
1972  * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
1973  *
1974  * If the element already has an ID then that is returned, otherwise unique ID is
1975  * generated, set on the element, and returned.
1976  *
1977  * @return {string|null} The ID of the focusable element
1978  */
1979 OO.ui.mixin.TabIndexedElement.prototype.getInputId = function () {
1980         var id;
1981
1982         if ( !this.$tabIndexed ) {
1983                 return null;
1984         }
1985         if ( !this.isLabelableNode( this.$tabIndexed ) ) {
1986                 return null;
1987         }
1988
1989         id = this.$tabIndexed.attr( 'id' );
1990         if ( id === undefined ) {
1991                 id = OO.ui.generateElementId();
1992                 this.$tabIndexed.attr( 'id', id );
1993         }
1994
1995         return id;
1996 };
1997
1998 /**
1999  * Whether the node is 'labelable' according to the HTML spec
2000  * (i.e., whether it can be interacted with through a `<label for="…">`).
2001  * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2002  *
2003  * @private
2004  * @param {jQuery} $node
2005  * @return {boolean}
2006  */
2007 OO.ui.mixin.TabIndexedElement.prototype.isLabelableNode = function ( $node ) {
2008         var
2009                 labelableTags = [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2010                 tagName = $node.prop( 'tagName' ).toLowerCase();
2011
2012         if ( tagName === 'input' && $node.attr( 'type' ) !== 'hidden' ) {
2013                 return true;
2014         }
2015         if ( labelableTags.indexOf( tagName ) !== -1 ) {
2016                 return true;
2017         }
2018         return false;
2019 };
2020
2021 /**
2022  * Focus this element.
2023  *
2024  * @chainable
2025  */
2026 OO.ui.mixin.TabIndexedElement.prototype.focus = function () {
2027         if ( !this.isDisabled() ) {
2028                 this.$tabIndexed.focus();
2029         }
2030         return this;
2031 };
2032
2033 /**
2034  * Blur this element.
2035  *
2036  * @chainable
2037  */
2038 OO.ui.mixin.TabIndexedElement.prototype.blur = function () {
2039         this.$tabIndexed.blur();
2040         return this;
2041 };
2042
2043 /**
2044  * @inheritdoc OO.ui.Widget
2045  */
2046 OO.ui.mixin.TabIndexedElement.prototype.simulateLabelClick = function () {
2047         this.focus();
2048 };
2049
2050 /**
2051  * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2052  * interface element that can be configured with access keys for accessibility.
2053  * See the [OOjs UI documentation on MediaWiki] [1] for examples.
2054  *
2055  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
2056  *
2057  * @abstract
2058  * @class
2059  *
2060  * @constructor
2061  * @param {Object} [config] Configuration options
2062  * @cfg {jQuery} [$button] The button element created by the class.
2063  *  If this configuration is omitted, the button element will use a generated `<a>`.
2064  * @cfg {boolean} [framed=true] Render the button with a frame
2065  */
2066 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
2067         // Configuration initialization
2068         config = config || {};
2069
2070         // Properties
2071         this.$button = null;
2072         this.framed = null;
2073         this.active = config.active !== undefined && config.active;
2074         this.onMouseUpHandler = this.onMouseUp.bind( this );
2075         this.onMouseDownHandler = this.onMouseDown.bind( this );
2076         this.onKeyDownHandler = this.onKeyDown.bind( this );
2077         this.onKeyUpHandler = this.onKeyUp.bind( this );
2078         this.onClickHandler = this.onClick.bind( this );
2079         this.onKeyPressHandler = this.onKeyPress.bind( this );
2080
2081         // Initialization
2082         this.$element.addClass( 'oo-ui-buttonElement' );
2083         this.toggleFramed( config.framed === undefined || config.framed );
2084         this.setButtonElement( config.$button || $( '<a>' ) );
2085 };
2086
2087 /* Setup */
2088
2089 OO.initClass( OO.ui.mixin.ButtonElement );
2090
2091 /* Static Properties */
2092
2093 /**
2094  * Cancel mouse down events.
2095  *
2096  * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
2097  * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
2098  * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
2099  * parent widget.
2100  *
2101  * @static
2102  * @inheritable
2103  * @property {boolean}
2104  */
2105 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
2106
2107 /* Events */
2108
2109 /**
2110  * A 'click' event is emitted when the button element is clicked.
2111  *
2112  * @event click
2113  */
2114
2115 /* Methods */
2116
2117 /**
2118  * Set the button element.
2119  *
2120  * This method is used to retarget a button mixin so that its functionality applies to
2121  * the specified button element instead of the one created by the class. If a button element
2122  * is already set, the method will remove the mixin’s effect on that element.
2123  *
2124  * @param {jQuery} $button Element to use as button
2125  */
2126 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
2127         if ( this.$button ) {
2128                 this.$button
2129                         .removeClass( 'oo-ui-buttonElement-button' )
2130                         .removeAttr( 'role accesskey' )
2131                         .off( {
2132                                 mousedown: this.onMouseDownHandler,
2133                                 keydown: this.onKeyDownHandler,
2134                                 click: this.onClickHandler,
2135                                 keypress: this.onKeyPressHandler
2136                         } );
2137         }
2138
2139         this.$button = $button
2140                 .addClass( 'oo-ui-buttonElement-button' )
2141                 .on( {
2142                         mousedown: this.onMouseDownHandler,
2143                         keydown: this.onKeyDownHandler,
2144                         click: this.onClickHandler,
2145                         keypress: this.onKeyPressHandler
2146                 } );
2147
2148         // Add `role="button"` on `<a>` elements, where it's needed
2149         // `toUppercase()` is added for XHTML documents
2150         if ( this.$button.prop( 'tagName' ).toUpperCase() === 'A' ) {
2151                 this.$button.attr( 'role', 'button' );
2152         }
2153 };
2154
2155 /**
2156  * Handles mouse down events.
2157  *
2158  * @protected
2159  * @param {jQuery.Event} e Mouse down event
2160  */
2161 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
2162         if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
2163                 return;
2164         }
2165         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
2166         // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2167         // reliably remove the pressed class
2168         this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
2169         // Prevent change of focus unless specifically configured otherwise
2170         if ( this.constructor.static.cancelButtonMouseDownEvents ) {
2171                 return false;
2172         }
2173 };
2174
2175 /**
2176  * Handles mouse up events.
2177  *
2178  * @protected
2179  * @param {MouseEvent} e Mouse up event
2180  */
2181 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
2182         if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
2183                 return;
2184         }
2185         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
2186         // Stop listening for mouseup, since we only needed this once
2187         this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
2188 };
2189
2190 /**
2191  * Handles mouse click events.
2192  *
2193  * @protected
2194  * @param {jQuery.Event} e Mouse click event
2195  * @fires click
2196  */
2197 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
2198         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
2199                 if ( this.emit( 'click' ) ) {
2200                         return false;
2201                 }
2202         }
2203 };
2204
2205 /**
2206  * Handles key down events.
2207  *
2208  * @protected
2209  * @param {jQuery.Event} e Key down event
2210  */
2211 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
2212         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
2213                 return;
2214         }
2215         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
2216         // Run the keyup handler no matter where the key is when the button is let go, so we can
2217         // reliably remove the pressed class
2218         this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
2219 };
2220
2221 /**
2222  * Handles key up events.
2223  *
2224  * @protected
2225  * @param {KeyboardEvent} e Key up event
2226  */
2227 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
2228         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
2229                 return;
2230         }
2231         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
2232         // Stop listening for keyup, since we only needed this once
2233         this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
2234 };
2235
2236 /**
2237  * Handles key press events.
2238  *
2239  * @protected
2240  * @param {jQuery.Event} e Key press event
2241  * @fires click
2242  */
2243 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
2244         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
2245                 if ( this.emit( 'click' ) ) {
2246                         return false;
2247                 }
2248         }
2249 };
2250
2251 /**
2252  * Check if button has a frame.
2253  *
2254  * @return {boolean} Button is framed
2255  */
2256 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
2257         return this.framed;
2258 };
2259
2260 /**
2261  * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2262  *
2263  * @param {boolean} [framed] Make button framed, omit to toggle
2264  * @chainable
2265  */
2266 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
2267         framed = framed === undefined ? !this.framed : !!framed;
2268         if ( framed !== this.framed ) {
2269                 this.framed = framed;
2270                 this.$element
2271                         .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
2272                         .toggleClass( 'oo-ui-buttonElement-framed', framed );
2273                 this.updateThemeClasses();
2274         }
2275
2276         return this;
2277 };
2278
2279 /**
2280  * Set the button's active state.
2281  *
2282  * The active state can be set on:
2283  *
2284  *  - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2285  *  - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2286  *  - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2287  *
2288  * @protected
2289  * @param {boolean} value Make button active
2290  * @chainable
2291  */
2292 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
2293         this.active = !!value;
2294         this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
2295         this.updateThemeClasses();
2296         return this;
2297 };
2298
2299 /**
2300  * Check if the button is active
2301  *
2302  * @protected
2303  * @return {boolean} The button is active
2304  */
2305 OO.ui.mixin.ButtonElement.prototype.isActive = function () {
2306         return this.active;
2307 };
2308
2309 /**
2310  * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2311  * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2312  * items from the group is done through the interface the class provides.
2313  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2314  *
2315  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
2316  *
2317  * @abstract
2318  * @mixins OO.EmitterList
2319  * @class
2320  *
2321  * @constructor
2322  * @param {Object} [config] Configuration options
2323  * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2324  *  is omitted, the group element will use a generated `<div>`.
2325  */
2326 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
2327         // Configuration initialization
2328         config = config || {};
2329
2330         // Mixin constructors
2331         OO.EmitterList.call( this, config );
2332
2333         // Properties
2334         this.$group = null;
2335
2336         // Initialization
2337         this.setGroupElement( config.$group || $( '<div>' ) );
2338 };
2339
2340 /* Setup */
2341
2342 OO.mixinClass( OO.ui.mixin.GroupElement, OO.EmitterList );
2343
2344 /* Events */
2345
2346 /**
2347  * @event change
2348  *
2349  * A change event is emitted when the set of selected items changes.
2350  *
2351  * @param {OO.ui.Element[]} items Items currently in the group
2352  */
2353
2354 /* Methods */
2355
2356 /**
2357  * Set the group element.
2358  *
2359  * If an element is already set, items will be moved to the new element.
2360  *
2361  * @param {jQuery} $group Element to use as group
2362  */
2363 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
2364         var i, len;
2365
2366         this.$group = $group;
2367         for ( i = 0, len = this.items.length; i < len; i++ ) {
2368                 this.$group.append( this.items[ i ].$element );
2369         }
2370 };
2371
2372 /**
2373  * Get an item by its data.
2374  *
2375  * Only the first item with matching data will be returned. To return all matching items,
2376  * use the #getItemsFromData method.
2377  *
2378  * @param {Object} data Item data to search for
2379  * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2380  */
2381 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
2382         var i, len, item,
2383                 hash = OO.getHash( data );
2384
2385         for ( i = 0, len = this.items.length; i < len; i++ ) {
2386                 item = this.items[ i ];
2387                 if ( hash === OO.getHash( item.getData() ) ) {
2388                         return item;
2389                 }
2390         }
2391
2392         return null;
2393 };
2394
2395 /**
2396  * Get items by their data.
2397  *
2398  * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2399  *
2400  * @param {Object} data Item data to search for
2401  * @return {OO.ui.Element[]} Items with equivalent data
2402  */
2403 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
2404         var i, len, item,
2405                 hash = OO.getHash( data ),
2406                 items = [];
2407
2408         for ( i = 0, len = this.items.length; i < len; i++ ) {
2409                 item = this.items[ i ];
2410                 if ( hash === OO.getHash( item.getData() ) ) {
2411                         items.push( item );
2412                 }
2413         }
2414
2415         return items;
2416 };
2417
2418 /**
2419  * Add items to the group.
2420  *
2421  * Items will be added to the end of the group array unless the optional `index` parameter specifies
2422  * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2423  *
2424  * @param {OO.ui.Element[]} items An array of items to add to the group
2425  * @param {number} [index] Index of the insertion point
2426  * @chainable
2427  */
2428 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
2429         // Mixin method
2430         OO.EmitterList.prototype.addItems.call( this, items, index );
2431
2432         this.emit( 'change', this.getItems() );
2433         return this;
2434 };
2435
2436 /**
2437  * @inheritdoc
2438  */
2439 OO.ui.mixin.GroupElement.prototype.moveItem = function ( items, newIndex ) {
2440         // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2441         this.insertItemElements( items, newIndex );
2442
2443         // Mixin method
2444         newIndex = OO.EmitterList.prototype.moveItem.call( this, items, newIndex );
2445
2446         return newIndex;
2447 };
2448
2449 /**
2450  * @inheritdoc
2451  */
2452 OO.ui.mixin.GroupElement.prototype.insertItem = function ( item, index ) {
2453         item.setElementGroup( this );
2454         this.insertItemElements( item, index );
2455
2456         // Mixin method
2457         index = OO.EmitterList.prototype.insertItem.call( this, item, index );
2458
2459         return index;
2460 };
2461
2462 /**
2463  * Insert elements into the group
2464  *
2465  * @private
2466  * @param {OO.ui.Element} itemWidget Item to insert
2467  * @param {number} index Insertion index
2468  */
2469 OO.ui.mixin.GroupElement.prototype.insertItemElements = function ( itemWidget, index ) {
2470         if ( index === undefined || index < 0 || index >= this.items.length ) {
2471                 this.$group.append( itemWidget.$element );
2472         } else if ( index === 0 ) {
2473                 this.$group.prepend( itemWidget.$element );
2474         } else {
2475                 this.items[ index ].$element.before( itemWidget.$element );
2476         }
2477 };
2478
2479 /**
2480  * Remove the specified items from a group.
2481  *
2482  * Removed items are detached (not removed) from the DOM so that they may be reused.
2483  * To remove all items from a group, you may wish to use the #clearItems method instead.
2484  *
2485  * @param {OO.ui.Element[]} items An array of items to remove
2486  * @chainable
2487  */
2488 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
2489         var i, len, item, index;
2490
2491         // Remove specific items elements
2492         for ( i = 0, len = items.length; i < len; i++ ) {
2493                 item = items[ i ];
2494                 index = this.items.indexOf( item );
2495                 if ( index !== -1 ) {
2496                         item.setElementGroup( null );
2497                         item.$element.detach();
2498                 }
2499         }
2500
2501         // Mixin method
2502         OO.EmitterList.prototype.removeItems.call( this, items );
2503
2504         this.emit( 'change', this.getItems() );
2505         return this;
2506 };
2507
2508 /**
2509  * Clear all items from the group.
2510  *
2511  * Cleared items are detached from the DOM, not removed, so that they may be reused.
2512  * To remove only a subset of items from a group, use the #removeItems method.
2513  *
2514  * @chainable
2515  */
2516 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
2517         var i, len;
2518
2519         // Remove all item elements
2520         for ( i = 0, len = this.items.length; i < len; i++ ) {
2521                 this.items[ i ].setElementGroup( null );
2522                 this.items[ i ].$element.detach();
2523         }
2524
2525         // Mixin method
2526         OO.EmitterList.prototype.clearItems.call( this );
2527
2528         this.emit( 'change', this.getItems() );
2529         return this;
2530 };
2531
2532 /**
2533  * IconElement is often mixed into other classes to generate an icon.
2534  * Icons are graphics, about the size of normal text. They are used to aid the user
2535  * in locating a control or to convey information in a space-efficient way. See the
2536  * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2537  * included in the library.
2538  *
2539  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2540  *
2541  * @abstract
2542  * @class
2543  *
2544  * @constructor
2545  * @param {Object} [config] Configuration options
2546  * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2547  *  the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2548  *  the icon element be set to an existing icon instead of the one generated by this class, set a
2549  *  value using a jQuery selection. For example:
2550  *
2551  *      // Use a <div> tag instead of a <span>
2552  *     $icon: $("<div>")
2553  *     // Use an existing icon element instead of the one generated by the class
2554  *     $icon: this.$element
2555  *     // Use an icon element from a child widget
2556  *     $icon: this.childwidget.$element
2557  * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2558  *  symbolic names.  A map is used for i18n purposes and contains a `default` icon
2559  *  name and additional names keyed by language code. The `default` name is used when no icon is keyed
2560  *  by the user's language.
2561  *
2562  *  Example of an i18n map:
2563  *
2564  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2565  *  See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2566  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2567  * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2568  *  text. The icon title is displayed when users move the mouse over the icon.
2569  */
2570 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
2571         // Configuration initialization
2572         config = config || {};
2573
2574         // Properties
2575         this.$icon = null;
2576         this.icon = null;
2577         this.iconTitle = null;
2578
2579         // Initialization
2580         this.setIcon( config.icon || this.constructor.static.icon );
2581         this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
2582         this.setIconElement( config.$icon || $( '<span>' ) );
2583 };
2584
2585 /* Setup */
2586
2587 OO.initClass( OO.ui.mixin.IconElement );
2588
2589 /* Static Properties */
2590
2591 /**
2592  * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2593  * for i18n purposes and contains a `default` icon name and additional names keyed by
2594  * language code. The `default` name is used when no icon is keyed by the user's language.
2595  *
2596  * Example of an i18n map:
2597  *
2598  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2599  *
2600  * Note: the static property will be overridden if the #icon configuration is used.
2601  *
2602  * @static
2603  * @inheritable
2604  * @property {Object|string}
2605  */
2606 OO.ui.mixin.IconElement.static.icon = null;
2607
2608 /**
2609  * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2610  * function that returns title text, or `null` for no title.
2611  *
2612  * The static property will be overridden if the #iconTitle configuration is used.
2613  *
2614  * @static
2615  * @inheritable
2616  * @property {string|Function|null}
2617  */
2618 OO.ui.mixin.IconElement.static.iconTitle = null;
2619
2620 /* Methods */
2621
2622 /**
2623  * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2624  * applies to the specified icon element instead of the one created by the class. If an icon
2625  * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2626  * and mixin methods will no longer affect the element.
2627  *
2628  * @param {jQuery} $icon Element to use as icon
2629  */
2630 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
2631         if ( this.$icon ) {
2632                 this.$icon
2633                         .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
2634                         .removeAttr( 'title' );
2635         }
2636
2637         this.$icon = $icon
2638                 .addClass( 'oo-ui-iconElement-icon' )
2639                 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
2640         if ( this.iconTitle !== null ) {
2641                 this.$icon.attr( 'title', this.iconTitle );
2642         }
2643
2644         this.updateThemeClasses();
2645 };
2646
2647 /**
2648  * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2649  * The icon parameter can also be set to a map of icon names. See the #icon config setting
2650  * for an example.
2651  *
2652  * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2653  *  by language code, or `null` to remove the icon.
2654  * @chainable
2655  */
2656 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
2657         icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
2658         icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
2659
2660         if ( this.icon !== icon ) {
2661                 if ( this.$icon ) {
2662                         if ( this.icon !== null ) {
2663                                 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
2664                         }
2665                         if ( icon !== null ) {
2666                                 this.$icon.addClass( 'oo-ui-icon-' + icon );
2667                         }
2668                 }
2669                 this.icon = icon;
2670         }
2671
2672         this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
2673         this.updateThemeClasses();
2674
2675         return this;
2676 };
2677
2678 /**
2679  * Set the icon title. Use `null` to remove the title.
2680  *
2681  * @param {string|Function|null} iconTitle A text string used as the icon title,
2682  *  a function that returns title text, or `null` for no title.
2683  * @chainable
2684  */
2685 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
2686         iconTitle =
2687                 ( typeof iconTitle === 'function' || ( typeof iconTitle === 'string' && iconTitle.length ) ) ?
2688                         OO.ui.resolveMsg( iconTitle ) : null;
2689
2690         if ( this.iconTitle !== iconTitle ) {
2691                 this.iconTitle = iconTitle;
2692                 if ( this.$icon ) {
2693                         if ( this.iconTitle !== null ) {
2694                                 this.$icon.attr( 'title', iconTitle );
2695                         } else {
2696                                 this.$icon.removeAttr( 'title' );
2697                         }
2698                 }
2699         }
2700
2701         return this;
2702 };
2703
2704 /**
2705  * Get the symbolic name of the icon.
2706  *
2707  * @return {string} Icon name
2708  */
2709 OO.ui.mixin.IconElement.prototype.getIcon = function () {
2710         return this.icon;
2711 };
2712
2713 /**
2714  * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2715  *
2716  * @return {string} Icon title text
2717  */
2718 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
2719         return this.iconTitle;
2720 };
2721
2722 /**
2723  * IndicatorElement is often mixed into other classes to generate an indicator.
2724  * Indicators are small graphics that are generally used in two ways:
2725  *
2726  * - To draw attention to the status of an item. For example, an indicator might be
2727  *   used to show that an item in a list has errors that need to be resolved.
2728  * - To clarify the function of a control that acts in an exceptional way (a button
2729  *   that opens a menu instead of performing an action directly, for example).
2730  *
2731  * For a list of indicators included in the library, please see the
2732  * [OOjs UI documentation on MediaWiki] [1].
2733  *
2734  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2735  *
2736  * @abstract
2737  * @class
2738  *
2739  * @constructor
2740  * @param {Object} [config] Configuration options
2741  * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2742  *  configuration is omitted, the indicator element will use a generated `<span>`.
2743  * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
2744  *  See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2745  *  in the library.
2746  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2747  * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2748  *  or a function that returns title text. The indicator title is displayed when users move
2749  *  the mouse over the indicator.
2750  */
2751 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
2752         // Configuration initialization
2753         config = config || {};
2754
2755         // Properties
2756         this.$indicator = null;
2757         this.indicator = null;
2758         this.indicatorTitle = null;
2759
2760         // Initialization
2761         this.setIndicator( config.indicator || this.constructor.static.indicator );
2762         this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
2763         this.setIndicatorElement( config.$indicator || $( '<span>' ) );
2764 };
2765
2766 /* Setup */
2767
2768 OO.initClass( OO.ui.mixin.IndicatorElement );
2769
2770 /* Static Properties */
2771
2772 /**
2773  * Symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
2774  * The static property will be overridden if the #indicator configuration is used.
2775  *
2776  * @static
2777  * @inheritable
2778  * @property {string|null}
2779  */
2780 OO.ui.mixin.IndicatorElement.static.indicator = null;
2781
2782 /**
2783  * A text string used as the indicator title, a function that returns title text, or `null`
2784  * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2785  *
2786  * @static
2787  * @inheritable
2788  * @property {string|Function|null}
2789  */
2790 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
2791
2792 /* Methods */
2793
2794 /**
2795  * Set the indicator element.
2796  *
2797  * If an element is already set, it will be cleaned up before setting up the new element.
2798  *
2799  * @param {jQuery} $indicator Element to use as indicator
2800  */
2801 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
2802         if ( this.$indicator ) {
2803                 this.$indicator
2804                         .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
2805                         .removeAttr( 'title' );
2806         }
2807
2808         this.$indicator = $indicator
2809                 .addClass( 'oo-ui-indicatorElement-indicator' )
2810                 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
2811         if ( this.indicatorTitle !== null ) {
2812                 this.$indicator.attr( 'title', this.indicatorTitle );
2813         }
2814
2815         this.updateThemeClasses();
2816 };
2817
2818 /**
2819  * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2820  *
2821  * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2822  * @chainable
2823  */
2824 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
2825         indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
2826
2827         if ( this.indicator !== indicator ) {
2828                 if ( this.$indicator ) {
2829                         if ( this.indicator !== null ) {
2830                                 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
2831                         }
2832                         if ( indicator !== null ) {
2833                                 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
2834                         }
2835                 }
2836                 this.indicator = indicator;
2837         }
2838
2839         this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
2840         this.updateThemeClasses();
2841
2842         return this;
2843 };
2844
2845 /**
2846  * Set the indicator title.
2847  *
2848  * The title is displayed when a user moves the mouse over the indicator.
2849  *
2850  * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2851  *   `null` for no indicator title
2852  * @chainable
2853  */
2854 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
2855         indicatorTitle =
2856                 ( typeof indicatorTitle === 'function' || ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ) ?
2857                         OO.ui.resolveMsg( indicatorTitle ) : null;
2858
2859         if ( this.indicatorTitle !== indicatorTitle ) {
2860                 this.indicatorTitle = indicatorTitle;
2861                 if ( this.$indicator ) {
2862                         if ( this.indicatorTitle !== null ) {
2863                                 this.$indicator.attr( 'title', indicatorTitle );
2864                         } else {
2865                                 this.$indicator.removeAttr( 'title' );
2866                         }
2867                 }
2868         }
2869
2870         return this;
2871 };
2872
2873 /**
2874  * Get the symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
2875  *
2876  * @return {string} Symbolic name of indicator
2877  */
2878 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
2879         return this.indicator;
2880 };
2881
2882 /**
2883  * Get the indicator title.
2884  *
2885  * The title is displayed when a user moves the mouse over the indicator.
2886  *
2887  * @return {string} Indicator title text
2888  */
2889 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
2890         return this.indicatorTitle;
2891 };
2892
2893 /**
2894  * LabelElement is often mixed into other classes to generate a label, which
2895  * helps identify the function of an interface element.
2896  * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2897  *
2898  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2899  *
2900  * @abstract
2901  * @class
2902  *
2903  * @constructor
2904  * @param {Object} [config] Configuration options
2905  * @cfg {jQuery} [$label] The label element created by the class. If this
2906  *  configuration is omitted, the label element will use a generated `<span>`.
2907  * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2908  *  as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2909  *  in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2910  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2911  */
2912 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
2913         // Configuration initialization
2914         config = config || {};
2915
2916         // Properties
2917         this.$label = null;
2918         this.label = null;
2919
2920         // Initialization
2921         this.setLabel( config.label || this.constructor.static.label );
2922         this.setLabelElement( config.$label || $( '<span>' ) );
2923 };
2924
2925 /* Setup */
2926
2927 OO.initClass( OO.ui.mixin.LabelElement );
2928
2929 /* Events */
2930
2931 /**
2932  * @event labelChange
2933  * @param {string} value
2934  */
2935
2936 /* Static Properties */
2937
2938 /**
2939  * The label text. The label can be specified as a plaintext string, a function that will
2940  * produce a string in the future, or `null` for no label. The static value will
2941  * be overridden if a label is specified with the #label config option.
2942  *
2943  * @static
2944  * @inheritable
2945  * @property {string|Function|null}
2946  */
2947 OO.ui.mixin.LabelElement.static.label = null;
2948
2949 /* Static methods */
2950
2951 /**
2952  * Highlight the first occurrence of the query in the given text
2953  *
2954  * @param {string} text Text
2955  * @param {string} query Query to find
2956  * @return {jQuery} Text with the first match of the query
2957  *  sub-string wrapped in highlighted span
2958  */
2959 OO.ui.mixin.LabelElement.static.highlightQuery = function ( text, query ) {
2960         var $result = $( '<span>' ),
2961                 offset = text.toLowerCase().indexOf( query.toLowerCase() );
2962
2963         if ( !query.length || offset === -1 ) {
2964                 return $result.text( text );
2965         }
2966         $result.append(
2967                 document.createTextNode( text.slice( 0, offset ) ),
2968                 $( '<span>' )
2969                         .addClass( 'oo-ui-labelElement-label-highlight' )
2970                         .text( text.slice( offset, offset + query.length ) ),
2971                 document.createTextNode( text.slice( offset + query.length ) )
2972         );
2973         return $result.contents();
2974 };
2975
2976 /* Methods */
2977
2978 /**
2979  * Set the label element.
2980  *
2981  * If an element is already set, it will be cleaned up before setting up the new element.
2982  *
2983  * @param {jQuery} $label Element to use as label
2984  */
2985 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
2986         if ( this.$label ) {
2987                 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
2988         }
2989
2990         this.$label = $label.addClass( 'oo-ui-labelElement-label' );
2991         this.setLabelContent( this.label );
2992 };
2993
2994 /**
2995  * Set the label.
2996  *
2997  * An empty string will result in the label being hidden. A string containing only whitespace will
2998  * be converted to a single `&nbsp;`.
2999  *
3000  * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
3001  *  text; or null for no label
3002  * @chainable
3003  */
3004 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
3005         label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
3006         label = ( ( typeof label === 'string' || label instanceof jQuery ) && label.length ) || ( label instanceof OO.ui.HtmlSnippet && label.toString().length ) ? label : null;
3007
3008         if ( this.label !== label ) {
3009                 if ( this.$label ) {
3010                         this.setLabelContent( label );
3011                 }
3012                 this.label = label;
3013                 this.emit( 'labelChange' );
3014         }
3015
3016         this.$element.toggleClass( 'oo-ui-labelElement', !!this.label );
3017
3018         return this;
3019 };
3020
3021 /**
3022  * Set the label as plain text with a highlighted query
3023  *
3024  * @param {string} text Text label to set
3025  * @param {string} query Substring of text to highlight
3026  * @chainable
3027  */
3028 OO.ui.mixin.LabelElement.prototype.setHighlightedQuery = function ( text, query ) {
3029         return this.setLabel( this.constructor.static.highlightQuery( text, query ) );
3030 };
3031
3032 /**
3033  * Get the label.
3034  *
3035  * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
3036  *  text; or null for no label
3037  */
3038 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
3039         return this.label;
3040 };
3041
3042 /**
3043  * Set the content of the label.
3044  *
3045  * Do not call this method until after the label element has been set by #setLabelElement.
3046  *
3047  * @private
3048  * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
3049  *  text; or null for no label
3050  */
3051 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
3052         if ( typeof label === 'string' ) {
3053                 if ( label.match( /^\s*$/ ) ) {
3054                         // Convert whitespace only string to a single non-breaking space
3055                         this.$label.html( '&nbsp;' );
3056                 } else {
3057                         this.$label.text( label );
3058                 }
3059         } else if ( label instanceof OO.ui.HtmlSnippet ) {
3060                 this.$label.html( label.toString() );
3061         } else if ( label instanceof jQuery ) {
3062                 this.$label.empty().append( label );
3063         } else {
3064                 this.$label.empty();
3065         }
3066 };
3067
3068 /**
3069  * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3070  * additional functionality to an element created by another class. The class provides
3071  * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3072  * which are used to customize the look and feel of a widget to better describe its
3073  * importance and functionality.
3074  *
3075  * The library currently contains the following styling flags for general use:
3076  *
3077  * - **progressive**:  Progressive styling is applied to convey that the widget will move the user forward in a process.
3078  * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
3079  * - **constructive**: Constructive styling is applied to convey that the widget will create something.
3080  *
3081  * The flags affect the appearance of the buttons:
3082  *
3083  *     @example
3084  *     // FlaggedElement is mixed into ButtonWidget to provide styling flags
3085  *     var button1 = new OO.ui.ButtonWidget( {
3086  *         label: 'Constructive',
3087  *         flags: 'constructive'
3088  *     } );
3089  *     var button2 = new OO.ui.ButtonWidget( {
3090  *         label: 'Destructive',
3091  *         flags: 'destructive'
3092  *     } );
3093  *     var button3 = new OO.ui.ButtonWidget( {
3094  *         label: 'Progressive',
3095  *         flags: 'progressive'
3096  *     } );
3097  *     $( 'body' ).append( button1.$element, button2.$element, button3.$element );
3098  *
3099  * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
3100  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
3101  *
3102  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
3103  *
3104  * @abstract
3105  * @class
3106  *
3107  * @constructor
3108  * @param {Object} [config] Configuration options
3109  * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
3110  *  Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
3111  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
3112  * @cfg {jQuery} [$flagged] The flagged element. By default,
3113  *  the flagged functionality is applied to the element created by the class ($element).
3114  *  If a different element is specified, the flagged functionality will be applied to it instead.
3115  */
3116 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
3117         // Configuration initialization
3118         config = config || {};
3119
3120         // Properties
3121         this.flags = {};
3122         this.$flagged = null;
3123
3124         // Initialization
3125         this.setFlags( config.flags );
3126         this.setFlaggedElement( config.$flagged || this.$element );
3127 };
3128
3129 /* Events */
3130
3131 /**
3132  * @event flag
3133  * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3134  * parameter contains the name of each modified flag and indicates whether it was
3135  * added or removed.
3136  *
3137  * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3138  * that the flag was added, `false` that the flag was removed.
3139  */
3140
3141 /* Methods */
3142
3143 /**
3144  * Set the flagged element.
3145  *
3146  * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3147  * If an element is already set, the method will remove the mixin’s effect on that element.
3148  *
3149  * @param {jQuery} $flagged Element that should be flagged
3150  */
3151 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
3152         var classNames = Object.keys( this.flags ).map( function ( flag ) {
3153                 return 'oo-ui-flaggedElement-' + flag;
3154         } ).join( ' ' );
3155
3156         if ( this.$flagged ) {
3157                 this.$flagged.removeClass( classNames );
3158         }
3159
3160         this.$flagged = $flagged.addClass( classNames );
3161 };
3162
3163 /**
3164  * Check if the specified flag is set.
3165  *
3166  * @param {string} flag Name of flag
3167  * @return {boolean} The flag is set
3168  */
3169 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
3170         // This may be called before the constructor, thus before this.flags is set
3171         return this.flags && ( flag in this.flags );
3172 };
3173
3174 /**
3175  * Get the names of all flags set.
3176  *
3177  * @return {string[]} Flag names
3178  */
3179 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
3180         // This may be called before the constructor, thus before this.flags is set
3181         return Object.keys( this.flags || {} );
3182 };
3183
3184 /**
3185  * Clear all flags.
3186  *
3187  * @chainable
3188  * @fires flag
3189  */
3190 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
3191         var flag, className,
3192                 changes = {},
3193                 remove = [],
3194                 classPrefix = 'oo-ui-flaggedElement-';
3195
3196         for ( flag in this.flags ) {
3197                 className = classPrefix + flag;
3198                 changes[ flag ] = false;
3199                 delete this.flags[ flag ];
3200                 remove.push( className );
3201         }
3202
3203         if ( this.$flagged ) {
3204                 this.$flagged.removeClass( remove.join( ' ' ) );
3205         }
3206
3207         this.updateThemeClasses();
3208         this.emit( 'flag', changes );
3209
3210         return this;
3211 };
3212
3213 /**
3214  * Add one or more flags.
3215  *
3216  * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3217  *  or an object keyed by flag name with a boolean value that indicates whether the flag should
3218  *  be added (`true`) or removed (`false`).
3219  * @chainable
3220  * @fires flag
3221  */
3222 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
3223         var i, len, flag, className,
3224                 changes = {},
3225                 add = [],
3226                 remove = [],
3227                 classPrefix = 'oo-ui-flaggedElement-';
3228
3229         if ( typeof flags === 'string' ) {
3230                 className = classPrefix + flags;
3231                 // Set
3232                 if ( !this.flags[ flags ] ) {
3233                         this.flags[ flags ] = true;
3234                         add.push( className );
3235                 }
3236         } else if ( Array.isArray( flags ) ) {
3237                 for ( i = 0, len = flags.length; i < len; i++ ) {
3238                         flag = flags[ i ];
3239                         className = classPrefix + flag;
3240                         // Set
3241                         if ( !this.flags[ flag ] ) {
3242                                 changes[ flag ] = true;
3243                                 this.flags[ flag ] = true;
3244                                 add.push( className );
3245                         }
3246                 }
3247         } else if ( OO.isPlainObject( flags ) ) {
3248                 for ( flag in flags ) {
3249                         className = classPrefix + flag;
3250                         if ( flags[ flag ] ) {
3251                                 // Set
3252                                 if ( !this.flags[ flag ] ) {
3253                                         changes[ flag ] = true;
3254                                         this.flags[ flag ] = true;
3255                                         add.push( className );
3256                                 }
3257                         } else {
3258                                 // Remove
3259                                 if ( this.flags[ flag ] ) {
3260                                         changes[ flag ] = false;
3261                                         delete this.flags[ flag ];
3262                                         remove.push( className );
3263                                 }
3264                         }
3265                 }
3266         }
3267
3268         if ( this.$flagged ) {
3269                 this.$flagged
3270                         .addClass( add.join( ' ' ) )
3271                         .removeClass( remove.join( ' ' ) );
3272         }
3273
3274         this.updateThemeClasses();
3275         this.emit( 'flag', changes );
3276
3277         return this;
3278 };
3279
3280 /**
3281  * TitledElement is mixed into other classes to provide a `title` attribute.
3282  * Titles are rendered by the browser and are made visible when the user moves
3283  * the mouse over the element. Titles are not visible on touch devices.
3284  *
3285  *     @example
3286  *     // TitledElement provides a 'title' attribute to the
3287  *     // ButtonWidget class
3288  *     var button = new OO.ui.ButtonWidget( {
3289  *         label: 'Button with Title',
3290  *         title: 'I am a button'
3291  *     } );
3292  *     $( 'body' ).append( button.$element );
3293  *
3294  * @abstract
3295  * @class
3296  *
3297  * @constructor
3298  * @param {Object} [config] Configuration options
3299  * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3300  *  If this config is omitted, the title functionality is applied to $element, the
3301  *  element created by the class.
3302  * @cfg {string|Function} [title] The title text or a function that returns text. If
3303  *  this config is omitted, the value of the {@link #static-title static title} property is used.
3304  */
3305 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
3306         // Configuration initialization
3307         config = config || {};
3308
3309         // Properties
3310         this.$titled = null;
3311         this.title = null;
3312
3313         // Initialization
3314         this.setTitle( config.title !== undefined ? config.title : this.constructor.static.title );
3315         this.setTitledElement( config.$titled || this.$element );
3316 };
3317
3318 /* Setup */
3319
3320 OO.initClass( OO.ui.mixin.TitledElement );
3321
3322 /* Static Properties */
3323
3324 /**
3325  * The title text, a function that returns text, or `null` for no title. The value of the static property
3326  * is overridden if the #title config option is used.
3327  *
3328  * @static
3329  * @inheritable
3330  * @property {string|Function|null}
3331  */
3332 OO.ui.mixin.TitledElement.static.title = null;
3333
3334 /* Methods */
3335
3336 /**
3337  * Set the titled element.
3338  *
3339  * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3340  * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3341  *
3342  * @param {jQuery} $titled Element that should use the 'titled' functionality
3343  */
3344 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
3345         if ( this.$titled ) {
3346                 this.$titled.removeAttr( 'title' );
3347         }
3348
3349         this.$titled = $titled;
3350         if ( this.title ) {
3351                 this.updateTitle();
3352         }
3353 };
3354
3355 /**
3356  * Set title.
3357  *
3358  * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3359  * @chainable
3360  */
3361 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
3362         title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
3363         title = ( typeof title === 'string' && title.length ) ? title : null;
3364
3365         if ( this.title !== title ) {
3366                 this.title = title;
3367                 this.updateTitle();
3368         }
3369
3370         return this;
3371 };
3372
3373 /**
3374  * Update the title attribute, in case of changes to title or accessKey.
3375  *
3376  * @protected
3377  * @chainable
3378  */
3379 OO.ui.mixin.TitledElement.prototype.updateTitle = function () {
3380         var title = this.getTitle();
3381         if ( this.$titled ) {
3382                 if ( title !== null ) {
3383                         // Only if this is an AccessKeyedElement
3384                         if ( this.formatTitleWithAccessKey ) {
3385                                 title = this.formatTitleWithAccessKey( title );
3386                         }
3387                         this.$titled.attr( 'title', title );
3388                 } else {
3389                         this.$titled.removeAttr( 'title' );
3390                 }
3391         }
3392         return this;
3393 };
3394
3395 /**
3396  * Get title.
3397  *
3398  * @return {string} Title string
3399  */
3400 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
3401         return this.title;
3402 };
3403
3404 /**
3405  * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3406  * Accesskeys allow an user to go to a specific element by using
3407  * a shortcut combination of a browser specific keys + the key
3408  * set to the field.
3409  *
3410  *     @example
3411  *     // AccessKeyedElement provides an 'accesskey' attribute to the
3412  *     // ButtonWidget class
3413  *     var button = new OO.ui.ButtonWidget( {
3414  *         label: 'Button with Accesskey',
3415  *         accessKey: 'k'
3416  *     } );
3417  *     $( 'body' ).append( button.$element );
3418  *
3419  * @abstract
3420  * @class
3421  *
3422  * @constructor
3423  * @param {Object} [config] Configuration options
3424  * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3425  *  If this config is omitted, the accesskey functionality is applied to $element, the
3426  *  element created by the class.
3427  * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3428  *  this config is omitted, no accesskey will be added.
3429  */
3430 OO.ui.mixin.AccessKeyedElement = function OoUiMixinAccessKeyedElement( config ) {
3431         // Configuration initialization
3432         config = config || {};
3433
3434         // Properties
3435         this.$accessKeyed = null;
3436         this.accessKey = null;
3437
3438         // Initialization
3439         this.setAccessKey( config.accessKey || null );
3440         this.setAccessKeyedElement( config.$accessKeyed || this.$element );
3441
3442         // If this is also a TitledElement and it initialized before we did, we may have
3443         // to update the title with the access key
3444         if ( this.updateTitle ) {
3445                 this.updateTitle();
3446         }
3447 };
3448
3449 /* Setup */
3450
3451 OO.initClass( OO.ui.mixin.AccessKeyedElement );
3452
3453 /* Static Properties */
3454
3455 /**
3456  * The access key, a function that returns a key, or `null` for no accesskey.
3457  *
3458  * @static
3459  * @inheritable
3460  * @property {string|Function|null}
3461  */
3462 OO.ui.mixin.AccessKeyedElement.static.accessKey = null;
3463
3464 /* Methods */
3465
3466 /**
3467  * Set the accesskeyed element.
3468  *
3469  * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3470  * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3471  *
3472  * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3473  */
3474 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKeyedElement = function ( $accessKeyed ) {
3475         if ( this.$accessKeyed ) {
3476                 this.$accessKeyed.removeAttr( 'accesskey' );
3477         }
3478
3479         this.$accessKeyed = $accessKeyed;
3480         if ( this.accessKey ) {
3481                 this.$accessKeyed.attr( 'accesskey', this.accessKey );
3482         }
3483 };
3484
3485 /**
3486  * Set accesskey.
3487  *
3488  * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3489  * @chainable
3490  */
3491 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKey = function ( accessKey ) {
3492         accessKey = typeof accessKey === 'string' ? OO.ui.resolveMsg( accessKey ) : null;
3493
3494         if ( this.accessKey !== accessKey ) {
3495                 if ( this.$accessKeyed ) {
3496                         if ( accessKey !== null ) {
3497                                 this.$accessKeyed.attr( 'accesskey', accessKey );
3498                         } else {
3499                                 this.$accessKeyed.removeAttr( 'accesskey' );
3500                         }
3501                 }
3502                 this.accessKey = accessKey;
3503
3504                 // Only if this is a TitledElement
3505                 if ( this.updateTitle ) {
3506                         this.updateTitle();
3507                 }
3508         }
3509
3510         return this;
3511 };
3512
3513 /**
3514  * Get accesskey.
3515  *
3516  * @return {string} accessKey string
3517  */
3518 OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
3519         return this.accessKey;
3520 };
3521
3522 /**
3523  * Add information about the access key to the element's tooltip label.
3524  * (This is only public for hacky usage in FieldLayout.)
3525  *
3526  * @param {string} title Tooltip label for `title` attribute
3527  * @return {string}
3528  */
3529 OO.ui.mixin.AccessKeyedElement.prototype.formatTitleWithAccessKey = function ( title ) {
3530         var accessKey;
3531
3532         if ( !this.$accessKeyed ) {
3533                 // Not initialized yet; the constructor will call updateTitle() which will rerun this function
3534                 return title;
3535         }
3536         // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the single key
3537         if ( $.fn.updateTooltipAccessKeys && $.fn.updateTooltipAccessKeys.getAccessKeyLabel ) {
3538                 accessKey = $.fn.updateTooltipAccessKeys.getAccessKeyLabel( this.$accessKeyed[ 0 ] );
3539         } else {
3540                 accessKey = this.getAccessKey();
3541         }
3542         if ( accessKey ) {
3543                 title += ' [' + accessKey + ']';
3544         }
3545         return title;
3546 };
3547
3548 /**
3549  * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3550  * feels, and functionality can be customized via the class’s configuration options
3551  * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3552  * and examples.
3553  *
3554  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3555  *
3556  *     @example
3557  *     // A button widget
3558  *     var button = new OO.ui.ButtonWidget( {
3559  *         label: 'Button with Icon',
3560  *         icon: 'trash',
3561  *         iconTitle: 'Remove'
3562  *     } );
3563  *     $( 'body' ).append( button.$element );
3564  *
3565  * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3566  *
3567  * @class
3568  * @extends OO.ui.Widget
3569  * @mixins OO.ui.mixin.ButtonElement
3570  * @mixins OO.ui.mixin.IconElement
3571  * @mixins OO.ui.mixin.IndicatorElement
3572  * @mixins OO.ui.mixin.LabelElement
3573  * @mixins OO.ui.mixin.TitledElement
3574  * @mixins OO.ui.mixin.FlaggedElement
3575  * @mixins OO.ui.mixin.TabIndexedElement
3576  * @mixins OO.ui.mixin.AccessKeyedElement
3577  *
3578  * @constructor
3579  * @param {Object} [config] Configuration options
3580  * @cfg {boolean} [active=false] Whether button should be shown as active
3581  * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3582  * @cfg {string} [target] The frame or window in which to open the hyperlink.
3583  * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3584  */
3585 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
3586         // Configuration initialization
3587         config = config || {};
3588
3589         // Parent constructor
3590         OO.ui.ButtonWidget.parent.call( this, config );
3591
3592         // Mixin constructors
3593         OO.ui.mixin.ButtonElement.call( this, config );
3594         OO.ui.mixin.IconElement.call( this, config );
3595         OO.ui.mixin.IndicatorElement.call( this, config );
3596         OO.ui.mixin.LabelElement.call( this, config );
3597         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
3598         OO.ui.mixin.FlaggedElement.call( this, config );
3599         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
3600         OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$button } ) );
3601
3602         // Properties
3603         this.href = null;
3604         this.target = null;
3605         this.noFollow = false;
3606
3607         // Events
3608         this.connect( this, { disable: 'onDisable' } );
3609
3610         // Initialization
3611         this.$button.append( this.$icon, this.$label, this.$indicator );
3612         this.$element
3613                 .addClass( 'oo-ui-buttonWidget' )
3614                 .append( this.$button );
3615         this.setActive( config.active );
3616         this.setHref( config.href );
3617         this.setTarget( config.target );
3618         this.setNoFollow( config.noFollow );
3619 };
3620
3621 /* Setup */
3622
3623 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
3624 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
3625 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
3626 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
3627 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
3628 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
3629 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
3630 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
3631 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
3632
3633 /* Static Properties */
3634
3635 /**
3636  * @static
3637  * @inheritdoc
3638  */
3639 OO.ui.ButtonWidget.static.cancelButtonMouseDownEvents = false;
3640
3641 /**
3642  * @static
3643  * @inheritdoc
3644  */
3645 OO.ui.ButtonWidget.static.tagName = 'span';
3646
3647 /* Methods */
3648
3649 /**
3650  * Get hyperlink location.
3651  *
3652  * @return {string} Hyperlink location
3653  */
3654 OO.ui.ButtonWidget.prototype.getHref = function () {
3655         return this.href;
3656 };
3657
3658 /**
3659  * Get hyperlink target.
3660  *
3661  * @return {string} Hyperlink target
3662  */
3663 OO.ui.ButtonWidget.prototype.getTarget = function () {
3664         return this.target;
3665 };
3666
3667 /**
3668  * Get search engine traversal hint.
3669  *
3670  * @return {boolean} Whether search engines should avoid traversing this hyperlink
3671  */
3672 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
3673         return this.noFollow;
3674 };
3675
3676 /**
3677  * Set hyperlink location.
3678  *
3679  * @param {string|null} href Hyperlink location, null to remove
3680  */
3681 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
3682         href = typeof href === 'string' ? href : null;
3683         if ( href !== null && !OO.ui.isSafeUrl( href ) ) {
3684                 href = './' + href;
3685         }
3686
3687         if ( href !== this.href ) {
3688                 this.href = href;
3689                 this.updateHref();
3690         }
3691
3692         return this;
3693 };
3694
3695 /**
3696  * Update the `href` attribute, in case of changes to href or
3697  * disabled state.
3698  *
3699  * @private
3700  * @chainable
3701  */
3702 OO.ui.ButtonWidget.prototype.updateHref = function () {
3703         if ( this.href !== null && !this.isDisabled() ) {
3704                 this.$button.attr( 'href', this.href );
3705         } else {
3706                 this.$button.removeAttr( 'href' );
3707         }
3708
3709         return this;
3710 };
3711
3712 /**
3713  * Handle disable events.
3714  *
3715  * @private
3716  * @param {boolean} disabled Element is disabled
3717  */
3718 OO.ui.ButtonWidget.prototype.onDisable = function () {
3719         this.updateHref();
3720 };
3721
3722 /**
3723  * Set hyperlink target.
3724  *
3725  * @param {string|null} target Hyperlink target, null to remove
3726  */
3727 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
3728         target = typeof target === 'string' ? target : null;
3729
3730         if ( target !== this.target ) {
3731                 this.target = target;
3732                 if ( target !== null ) {
3733                         this.$button.attr( 'target', target );
3734                 } else {
3735                         this.$button.removeAttr( 'target' );
3736                 }
3737         }
3738
3739         return this;
3740 };
3741
3742 /**
3743  * Set search engine traversal hint.
3744  *
3745  * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3746  */
3747 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
3748         noFollow = typeof noFollow === 'boolean' ? noFollow : true;
3749
3750         if ( noFollow !== this.noFollow ) {
3751                 this.noFollow = noFollow;
3752                 if ( noFollow ) {
3753                         this.$button.attr( 'rel', 'nofollow' );
3754                 } else {
3755                         this.$button.removeAttr( 'rel' );
3756                 }
3757         }
3758
3759         return this;
3760 };
3761
3762 // Override method visibility hints from ButtonElement
3763 /**
3764  * @method setActive
3765  * @inheritdoc
3766  */
3767 /**
3768  * @method isActive
3769  * @inheritdoc
3770  */
3771
3772 /**
3773  * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3774  * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3775  * removed, and cleared from the group.
3776  *
3777  *     @example
3778  *     // Example: A ButtonGroupWidget with two buttons
3779  *     var button1 = new OO.ui.PopupButtonWidget( {
3780  *         label: 'Select a category',
3781  *         icon: 'menu',
3782  *         popup: {
3783  *             $content: $( '<p>List of categories...</p>' ),
3784  *             padded: true,
3785  *             align: 'left'
3786  *         }
3787  *     } );
3788  *     var button2 = new OO.ui.ButtonWidget( {
3789  *         label: 'Add item'
3790  *     });
3791  *     var buttonGroup = new OO.ui.ButtonGroupWidget( {
3792  *         items: [button1, button2]
3793  *     } );
3794  *     $( 'body' ).append( buttonGroup.$element );
3795  *
3796  * @class
3797  * @extends OO.ui.Widget
3798  * @mixins OO.ui.mixin.GroupElement
3799  *
3800  * @constructor
3801  * @param {Object} [config] Configuration options
3802  * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3803  */
3804 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
3805         // Configuration initialization
3806         config = config || {};
3807
3808         // Parent constructor
3809         OO.ui.ButtonGroupWidget.parent.call( this, config );
3810
3811         // Mixin constructors
3812         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
3813
3814         // Initialization
3815         this.$element.addClass( 'oo-ui-buttonGroupWidget' );
3816         if ( Array.isArray( config.items ) ) {
3817                 this.addItems( config.items );
3818         }
3819 };
3820
3821 /* Setup */
3822
3823 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
3824 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
3825
3826 /* Static Properties */
3827
3828 /**
3829  * @static
3830  * @inheritdoc
3831  */
3832 OO.ui.ButtonGroupWidget.static.tagName = 'span';
3833
3834 /* Methods */
3835
3836 /**
3837  * Focus the widget
3838  *
3839  * @chainable
3840  */
3841 OO.ui.ButtonGroupWidget.prototype.focus = function () {
3842         if ( !this.isDisabled() ) {
3843                 if ( this.items[ 0 ] ) {
3844                         this.items[ 0 ].focus();
3845                 }
3846         }
3847         return this;
3848 };
3849
3850 /**
3851  * @inheritdoc
3852  */
3853 OO.ui.ButtonGroupWidget.prototype.simulateLabelClick = function () {
3854         this.focus();
3855 };
3856
3857 /**
3858  * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3859  * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3860  * for a list of icons included in the library.
3861  *
3862  *     @example
3863  *     // An icon widget with a label
3864  *     var myIcon = new OO.ui.IconWidget( {
3865  *         icon: 'help',
3866  *         iconTitle: 'Help'
3867  *      } );
3868  *      // Create a label.
3869  *      var iconLabel = new OO.ui.LabelWidget( {
3870  *          label: 'Help'
3871  *      } );
3872  *      $( 'body' ).append( myIcon.$element, iconLabel.$element );
3873  *
3874  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3875  *
3876  * @class
3877  * @extends OO.ui.Widget
3878  * @mixins OO.ui.mixin.IconElement
3879  * @mixins OO.ui.mixin.TitledElement
3880  * @mixins OO.ui.mixin.FlaggedElement
3881  *
3882  * @constructor
3883  * @param {Object} [config] Configuration options
3884  */
3885 OO.ui.IconWidget = function OoUiIconWidget( config ) {
3886         // Configuration initialization
3887         config = config || {};
3888
3889         // Parent constructor
3890         OO.ui.IconWidget.parent.call( this, config );
3891
3892         // Mixin constructors
3893         OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
3894         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
3895         OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
3896
3897         // Initialization
3898         this.$element.addClass( 'oo-ui-iconWidget' );
3899 };
3900
3901 /* Setup */
3902
3903 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
3904 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
3905 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
3906 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
3907
3908 /* Static Properties */
3909
3910 /**
3911  * @static
3912  * @inheritdoc
3913  */
3914 OO.ui.IconWidget.static.tagName = 'span';
3915
3916 /**
3917  * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3918  * attention to the status of an item or to clarify the function of a control. For a list of
3919  * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3920  *
3921  *     @example
3922  *     // Example of an indicator widget
3923  *     var indicator1 = new OO.ui.IndicatorWidget( {
3924  *         indicator: 'alert'
3925  *     } );
3926  *
3927  *     // Create a fieldset layout to add a label
3928  *     var fieldset = new OO.ui.FieldsetLayout();
3929  *     fieldset.addItems( [
3930  *         new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3931  *     ] );
3932  *     $( 'body' ).append( fieldset.$element );
3933  *
3934  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3935  *
3936  * @class
3937  * @extends OO.ui.Widget
3938  * @mixins OO.ui.mixin.IndicatorElement
3939  * @mixins OO.ui.mixin.TitledElement
3940  *
3941  * @constructor
3942  * @param {Object} [config] Configuration options
3943  */
3944 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
3945         // Configuration initialization
3946         config = config || {};
3947
3948         // Parent constructor
3949         OO.ui.IndicatorWidget.parent.call( this, config );
3950
3951         // Mixin constructors
3952         OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
3953         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
3954
3955         // Initialization
3956         this.$element.addClass( 'oo-ui-indicatorWidget' );
3957 };
3958
3959 /* Setup */
3960
3961 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
3962 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
3963 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
3964
3965 /* Static Properties */
3966
3967 /**
3968  * @static
3969  * @inheritdoc
3970  */
3971 OO.ui.IndicatorWidget.static.tagName = 'span';
3972
3973 /**
3974  * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3975  * be configured with a `label` option that is set to a string, a label node, or a function:
3976  *
3977  * - String: a plaintext string
3978  * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3979  *   label that includes a link or special styling, such as a gray color or additional graphical elements.
3980  * - Function: a function that will produce a string in the future. Functions are used
3981  *   in cases where the value of the label is not currently defined.
3982  *
3983  * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3984  * will come into focus when the label is clicked.
3985  *
3986  *     @example
3987  *     // Examples of LabelWidgets
3988  *     var label1 = new OO.ui.LabelWidget( {
3989  *         label: 'plaintext label'
3990  *     } );
3991  *     var label2 = new OO.ui.LabelWidget( {
3992  *         label: $( '<a href="default.html">jQuery label</a>' )
3993  *     } );
3994  *     // Create a fieldset layout with fields for each example
3995  *     var fieldset = new OO.ui.FieldsetLayout();
3996  *     fieldset.addItems( [
3997  *         new OO.ui.FieldLayout( label1 ),
3998  *         new OO.ui.FieldLayout( label2 )
3999  *     ] );
4000  *     $( 'body' ).append( fieldset.$element );
4001  *
4002  * @class
4003  * @extends OO.ui.Widget
4004  * @mixins OO.ui.mixin.LabelElement
4005  * @mixins OO.ui.mixin.TitledElement
4006  *
4007  * @constructor
4008  * @param {Object} [config] Configuration options
4009  * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4010  *  Clicking the label will focus the specified input field.
4011  */
4012 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
4013         // Configuration initialization
4014         config = config || {};
4015
4016         // Parent constructor
4017         OO.ui.LabelWidget.parent.call( this, config );
4018
4019         // Mixin constructors
4020         OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
4021         OO.ui.mixin.TitledElement.call( this, config );
4022
4023         // Properties
4024         this.input = config.input;
4025
4026         // Initialization
4027         if ( this.input ) {
4028                 if ( this.input.getInputId() ) {
4029                         this.$element.attr( 'for', this.input.getInputId() );
4030                 } else {
4031                         this.$label.on( 'click', function () {
4032                                 this.input.simulateLabelClick();
4033                                 return false;
4034                         }.bind( this ) );
4035                 }
4036         }
4037         this.$element.addClass( 'oo-ui-labelWidget' );
4038 };
4039
4040 /* Setup */
4041
4042 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
4043 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
4044 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
4045
4046 /* Static Properties */
4047
4048 /**
4049  * @static
4050  * @inheritdoc
4051  */
4052 OO.ui.LabelWidget.static.tagName = 'label';
4053
4054 /**
4055  * PendingElement is a mixin that is used to create elements that notify users that something is happening
4056  * and that they should wait before proceeding. The pending state is visually represented with a pending
4057  * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
4058  * field of a {@link OO.ui.TextInputWidget text input widget}.
4059  *
4060  * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
4061  * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
4062  * in process dialogs.
4063  *
4064  *     @example
4065  *     function MessageDialog( config ) {
4066  *         MessageDialog.parent.call( this, config );
4067  *     }
4068  *     OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4069  *
4070  *     MessageDialog.static.name = 'myMessageDialog';
4071  *     MessageDialog.static.actions = [
4072  *         { action: 'save', label: 'Done', flags: 'primary' },
4073  *         { label: 'Cancel', flags: 'safe' }
4074  *     ];
4075  *
4076  *     MessageDialog.prototype.initialize = function () {
4077  *         MessageDialog.parent.prototype.initialize.apply( this, arguments );
4078  *         this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
4079  *         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>' );
4080  *         this.$body.append( this.content.$element );
4081  *     };
4082  *     MessageDialog.prototype.getBodyHeight = function () {
4083  *         return 100;
4084  *     }
4085  *     MessageDialog.prototype.getActionProcess = function ( action ) {
4086  *         var dialog = this;
4087  *         if ( action === 'save' ) {
4088  *             dialog.getActions().get({actions: 'save'})[0].pushPending();
4089  *             return new OO.ui.Process()
4090  *             .next( 1000 )
4091  *             .next( function () {
4092  *                 dialog.getActions().get({actions: 'save'})[0].popPending();
4093  *             } );
4094  *         }
4095  *         return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4096  *     };
4097  *
4098  *     var windowManager = new OO.ui.WindowManager();
4099  *     $( 'body' ).append( windowManager.$element );
4100  *
4101  *     var dialog = new MessageDialog();
4102  *     windowManager.addWindows( [ dialog ] );
4103  *     windowManager.openWindow( dialog );
4104  *
4105  * @abstract
4106  * @class
4107  *
4108  * @constructor
4109  * @param {Object} [config] Configuration options
4110  * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4111  */
4112 OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
4113         // Configuration initialization
4114         config = config || {};
4115
4116         // Properties
4117         this.pending = 0;
4118         this.$pending = null;
4119
4120         // Initialisation
4121         this.setPendingElement( config.$pending || this.$element );
4122 };
4123
4124 /* Setup */
4125
4126 OO.initClass( OO.ui.mixin.PendingElement );
4127
4128 /* Methods */
4129
4130 /**
4131  * Set the pending element (and clean up any existing one).
4132  *
4133  * @param {jQuery} $pending The element to set to pending.
4134  */
4135 OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
4136         if ( this.$pending ) {
4137                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
4138         }
4139
4140         this.$pending = $pending;
4141         if ( this.pending > 0 ) {
4142                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
4143         }
4144 };
4145
4146 /**
4147  * Check if an element is pending.
4148  *
4149  * @return {boolean} Element is pending
4150  */
4151 OO.ui.mixin.PendingElement.prototype.isPending = function () {
4152         return !!this.pending;
4153 };
4154
4155 /**
4156  * Increase the pending counter. The pending state will remain active until the counter is zero
4157  * (i.e., the number of calls to #pushPending and #popPending is the same).
4158  *
4159  * @chainable
4160  */
4161 OO.ui.mixin.PendingElement.prototype.pushPending = function () {
4162         if ( this.pending === 0 ) {
4163                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
4164                 this.updateThemeClasses();
4165         }
4166         this.pending++;
4167
4168         return this;
4169 };
4170
4171 /**
4172  * Decrease the pending counter. The pending state will remain active until the counter is zero
4173  * (i.e., the number of calls to #pushPending and #popPending is the same).
4174  *
4175  * @chainable
4176  */
4177 OO.ui.mixin.PendingElement.prototype.popPending = function () {
4178         if ( this.pending === 1 ) {
4179                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
4180                 this.updateThemeClasses();
4181         }
4182         this.pending = Math.max( 0, this.pending - 1 );
4183
4184         return this;
4185 };
4186
4187 /**
4188  * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4189  * in the document (for example, in an OO.ui.Window's $overlay).
4190  *
4191  * The elements's position is automatically calculated and maintained when window is resized or the
4192  * page is scrolled. If you reposition the container manually, you have to call #position to make
4193  * sure the element is still placed correctly.
4194  *
4195  * As positioning is only possible when both the element and the container are attached to the DOM
4196  * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4197  * the #toggle method to display a floating popup, for example.
4198  *
4199  * @abstract
4200  * @class
4201  *
4202  * @constructor
4203  * @param {Object} [config] Configuration options
4204  * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4205  * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4206  * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4207  *  'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4208  *  'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4209  *  'top': Align the top edge with $floatableContainer's top edge
4210  *  'bottom': Align the bottom edge with $floatableContainer's bottom edge
4211  *  'center': Vertically align the center with $floatableContainer's center
4212  * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4213  *  'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4214  *  'after': Directly after $floatableContainer, algining f's start edge with fC's end edge
4215  *  'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4216  *  'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4217  *  'center': Horizontally align the center with $floatableContainer's center
4218  * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4219  *  is out of view
4220  */
4221 OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
4222         // Configuration initialization
4223         config = config || {};
4224
4225         // Properties
4226         this.$floatable = null;
4227         this.$floatableContainer = null;
4228         this.$floatableWindow = null;
4229         this.$floatableClosestScrollable = null;
4230         this.onFloatableScrollHandler = this.position.bind( this );
4231         this.onFloatableWindowResizeHandler = this.position.bind( this );
4232
4233         // Initialization
4234         this.setFloatableContainer( config.$floatableContainer );
4235         this.setFloatableElement( config.$floatable || this.$element );
4236         this.setVerticalPosition( config.verticalPosition || 'below' );
4237         this.setHorizontalPosition( config.horizontalPosition || 'start' );
4238         this.hideWhenOutOfView = config.hideWhenOutOfView === undefined ? true : !!config.hideWhenOutOfView;
4239 };
4240
4241 /* Methods */
4242
4243 /**
4244  * Set floatable element.
4245  *
4246  * If an element is already set, it will be cleaned up before setting up the new element.
4247  *
4248  * @param {jQuery} $floatable Element to make floatable
4249  */
4250 OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
4251         if ( this.$floatable ) {
4252                 this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
4253                 this.$floatable.css( { left: '', top: '' } );
4254         }
4255
4256         this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
4257         this.position();
4258 };
4259
4260 /**
4261  * Set floatable container.
4262  *
4263  * The element will be positioned relative to the specified container.
4264  *
4265  * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4266  */
4267 OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
4268         this.$floatableContainer = $floatableContainer;
4269         if ( this.$floatable ) {
4270                 this.position();
4271         }
4272 };
4273
4274 /**
4275  * Change how the element is positioned vertically.
4276  *
4277  * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4278  */
4279 OO.ui.mixin.FloatableElement.prototype.setVerticalPosition = function ( position ) {
4280         if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position ) === -1 ) {
4281                 throw new Error( 'Invalid value for vertical position: ' + position );
4282         }
4283         if ( this.verticalPosition !== position ) {
4284                 this.verticalPosition = position;
4285                 if ( this.$floatable ) {
4286                         this.position();
4287                 }
4288         }
4289 };
4290
4291 /**
4292  * Change how the element is positioned horizontally.
4293  *
4294  * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4295  */
4296 OO.ui.mixin.FloatableElement.prototype.setHorizontalPosition = function ( position ) {
4297         if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position ) === -1 ) {
4298                 throw new Error( 'Invalid value for horizontal position: ' + position );
4299         }
4300         if ( this.horizontalPosition !== position ) {
4301                 this.horizontalPosition = position;
4302                 if ( this.$floatable ) {
4303                         this.position();
4304                 }
4305         }
4306 };
4307
4308 /**
4309  * Toggle positioning.
4310  *
4311  * Do not turn positioning on until after the element is attached to the DOM and visible.
4312  *
4313  * @param {boolean} [positioning] Enable positioning, omit to toggle
4314  * @chainable
4315  */
4316 OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
4317         var closestScrollableOfContainer;
4318
4319         if ( !this.$floatable || !this.$floatableContainer ) {
4320                 return this;
4321         }
4322
4323         positioning = positioning === undefined ? !this.positioning : !!positioning;
4324
4325         if ( positioning && !this.warnedUnattached && !this.isElementAttached() ) {
4326                 OO.ui.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4327                 this.warnedUnattached = true;
4328         }
4329
4330         if ( this.positioning !== positioning ) {
4331                 this.positioning = positioning;
4332
4333                 this.needsCustomPosition =
4334                         this.verticalPostion !== 'below' ||
4335                         this.horizontalPosition !== 'start' ||
4336                         !OO.ui.contains( this.$floatableContainer[ 0 ], this.$floatable[ 0 ] );
4337
4338                 closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
4339                 // If the scrollable is the root, we have to listen to scroll events
4340                 // on the window because of browser inconsistencies.
4341                 if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
4342                         closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
4343                 }
4344
4345                 if ( positioning ) {
4346                         this.$floatableWindow = $( this.getElementWindow() );
4347                         this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
4348
4349                         this.$floatableClosestScrollable = $( closestScrollableOfContainer );
4350                         this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
4351
4352                         // Initial position after visible
4353                         this.position();
4354                 } else {
4355                         if ( this.$floatableWindow ) {
4356                                 this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
4357                                 this.$floatableWindow = null;
4358                         }
4359
4360                         if ( this.$floatableClosestScrollable ) {
4361                                 this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
4362                                 this.$floatableClosestScrollable = null;
4363                         }
4364
4365                         this.$floatable.css( { left: '', right: '', top: '' } );
4366                 }
4367         }
4368
4369         return this;
4370 };
4371
4372 /**
4373  * Check whether the bottom edge of the given element is within the viewport of the given container.
4374  *
4375  * @private
4376  * @param {jQuery} $element
4377  * @param {jQuery} $container
4378  * @return {boolean}
4379  */
4380 OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
4381         var elemRect, contRect, topEdgeInBounds, bottomEdgeInBounds, leftEdgeInBounds, rightEdgeInBounds,
4382                 startEdgeInBounds, endEdgeInBounds,
4383                 direction = $element.css( 'direction' );
4384
4385         elemRect = $element[ 0 ].getBoundingClientRect();
4386         if ( $container[ 0 ] === window ) {
4387                 contRect = {
4388                         top: 0,
4389                         left: 0,
4390                         right: document.documentElement.clientWidth,
4391                         bottom: document.documentElement.clientHeight
4392                 };
4393         } else {
4394                 contRect = $container[ 0 ].getBoundingClientRect();
4395         }
4396
4397         topEdgeInBounds = elemRect.top >= contRect.top && elemRect.top <= contRect.bottom;
4398         bottomEdgeInBounds = elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom;
4399         leftEdgeInBounds = elemRect.left >= contRect.left && elemRect.left <= contRect.right;
4400         rightEdgeInBounds = elemRect.right >= contRect.left && elemRect.right <= contRect.right;
4401         if ( direction === 'rtl' ) {
4402                 startEdgeInBounds = rightEdgeInBounds;
4403                 endEdgeInBounds = leftEdgeInBounds;
4404         } else {
4405                 startEdgeInBounds = leftEdgeInBounds;
4406                 endEdgeInBounds = rightEdgeInBounds;
4407         }
4408
4409         if ( this.verticalPosition === 'below' && !bottomEdgeInBounds ) {
4410                 return false;
4411         }
4412         if ( this.verticalPosition === 'above' && !topEdgeInBounds ) {
4413                 return false;
4414         }
4415         if ( this.horizontalPosition === 'before' && !startEdgeInBounds ) {
4416                 return false;
4417         }
4418         if ( this.horizontalPosition === 'after' && !endEdgeInBounds ) {
4419                 return false;
4420         }
4421
4422         // The other positioning values are all about being inside the container,
4423         // so in those cases all we care about is that any part of the container is visible.
4424         return elemRect.top <= contRect.bottom && elemRect.bottom >= contRect.top &&
4425                 elemRect.left <= contRect.right && elemRect.right >= contRect.left;
4426 };
4427
4428 /**
4429  * Position the floatable below its container.
4430  *
4431  * This should only be done when both of them are attached to the DOM and visible.
4432  *
4433  * @chainable
4434  */
4435 OO.ui.mixin.FloatableElement.prototype.position = function () {
4436         if ( !this.positioning ) {
4437                 return this;
4438         }
4439
4440         if ( !(
4441                 // To continue, some things need to be true:
4442                 // The element must actually be in the DOM
4443                 this.isElementAttached() && (
4444                         // The closest scrollable is the current window
4445                         this.$floatableClosestScrollable[ 0 ] === this.getElementWindow() ||
4446                         // OR is an element in the element's DOM
4447                         $.contains( this.getElementDocument(), this.$floatableClosestScrollable[ 0 ] )
4448                 )
4449         ) ) {
4450                 // Abort early if important parts of the widget are no longer attached to the DOM
4451                 return this;
4452         }
4453
4454         if ( this.hideWhenOutOfView && !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
4455                 this.$floatable.addClass( 'oo-ui-element-hidden' );
4456                 return this;
4457         } else {
4458                 this.$floatable.removeClass( 'oo-ui-element-hidden' );
4459         }
4460
4461         if ( !this.needsCustomPosition ) {
4462                 return this;
4463         }
4464
4465         this.$floatable.css( this.computePosition() );
4466
4467         // We updated the position, so re-evaluate the clipping state.
4468         // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4469         // will not notice the need to update itself.)
4470         // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4471         // it not listen to the right events in the right places?
4472         if ( this.clip ) {
4473                 this.clip();
4474         }
4475
4476         return this;
4477 };
4478
4479 /**
4480  * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4481  * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4482  * but may be overridden by subclasses if they want to change or add to the positioning logic.
4483  *
4484  * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4485  */
4486 OO.ui.mixin.FloatableElement.prototype.computePosition = function () {
4487         var isBody, scrollableX, scrollableY, containerPos,
4488                 horizScrollbarHeight, vertScrollbarWidth, scrollTop, scrollLeft,
4489                 newPos = { top: '', left: '', bottom: '', right: '' },
4490                 direction = this.$floatableContainer.css( 'direction' ),
4491                 $offsetParent = this.$floatable.offsetParent();
4492
4493         if ( $offsetParent.is( 'html' ) ) {
4494                 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4495                 // <html> element, but they do work on the <body>
4496                 $offsetParent = $( $offsetParent[ 0 ].ownerDocument.body );
4497         }
4498         isBody = $offsetParent.is( 'body' );
4499         scrollableX = $offsetParent.css( 'overflow-x' ) === 'scroll' || $offsetParent.css( 'overflow-x' ) === 'auto';
4500         scrollableY = $offsetParent.css( 'overflow-y' ) === 'scroll' || $offsetParent.css( 'overflow-y' ) === 'auto';
4501
4502         vertScrollbarWidth = $offsetParent.innerWidth() - $offsetParent.prop( 'clientWidth' );
4503         horizScrollbarHeight = $offsetParent.innerHeight() - $offsetParent.prop( 'clientHeight' );
4504         // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4505         // or if it isn't scrollable
4506         scrollTop = scrollableY && !isBody ? $offsetParent.scrollTop() : 0;
4507         scrollLeft = scrollableX && !isBody ? OO.ui.Element.static.getScrollLeft( $offsetParent[ 0 ] ) : 0;
4508
4509         // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4510         // if the <body> has a margin
4511         containerPos = isBody ?
4512                 this.$floatableContainer.offset() :
4513                 OO.ui.Element.static.getRelativePosition( this.$floatableContainer, $offsetParent );
4514         containerPos.bottom = containerPos.top + this.$floatableContainer.outerHeight();
4515         containerPos.right = containerPos.left + this.$floatableContainer.outerWidth();
4516         containerPos.start = direction === 'rtl' ? containerPos.right : containerPos.left;
4517         containerPos.end = direction === 'rtl' ? containerPos.left : containerPos.right;
4518
4519         if ( this.verticalPosition === 'below' ) {
4520                 newPos.top = containerPos.bottom;
4521         } else if ( this.verticalPosition === 'above' ) {
4522                 newPos.bottom = $offsetParent.outerHeight() - containerPos.top;
4523         } else if ( this.verticalPosition === 'top' ) {
4524                 newPos.top = containerPos.top;
4525         } else if ( this.verticalPosition === 'bottom' ) {
4526                 newPos.bottom = $offsetParent.outerHeight() - containerPos.bottom;
4527         } else if ( this.verticalPosition === 'center' ) {
4528                 newPos.top = containerPos.top +
4529                         ( this.$floatableContainer.height() - this.$floatable.height() ) / 2;
4530         }
4531
4532         if ( this.horizontalPosition === 'before' ) {
4533                 newPos.end = containerPos.start;
4534         } else if ( this.horizontalPosition === 'after' ) {
4535                 newPos.start = containerPos.end;
4536         } else if ( this.horizontalPosition === 'start' ) {
4537                 newPos.start = containerPos.start;
4538         } else if ( this.horizontalPosition === 'end' ) {
4539                 newPos.end = containerPos.end;
4540         } else if ( this.horizontalPosition === 'center' ) {
4541                 newPos.left = containerPos.left +
4542                         ( this.$floatableContainer.width() - this.$floatable.width() ) / 2;
4543         }
4544
4545         if ( newPos.start !== undefined ) {
4546                 if ( direction === 'rtl' ) {
4547                         newPos.right = ( isBody ? $( $offsetParent[ 0 ].ownerDocument.documentElement ) : $offsetParent ).outerWidth() - newPos.start;
4548                 } else {
4549                         newPos.left = newPos.start;
4550                 }
4551                 delete newPos.start;
4552         }
4553         if ( newPos.end !== undefined ) {
4554                 if ( direction === 'rtl' ) {
4555                         newPos.left = newPos.end;
4556                 } else {
4557                         newPos.right = ( isBody ? $( $offsetParent[ 0 ].ownerDocument.documentElement ) : $offsetParent ).outerWidth() - newPos.end;
4558                 }
4559                 delete newPos.end;
4560         }
4561
4562         // Account for scroll position
4563         if ( newPos.top !== '' ) {
4564                 newPos.top += scrollTop;
4565         }
4566         if ( newPos.bottom !== '' ) {
4567                 newPos.bottom -= scrollTop;
4568         }
4569         if ( newPos.left !== '' ) {
4570                 newPos.left += scrollLeft;
4571         }
4572         if ( newPos.right !== '' ) {
4573                 newPos.right -= scrollLeft;
4574         }
4575
4576         // Account for scrollbar gutter
4577         if ( newPos.bottom !== '' ) {
4578                 newPos.bottom -= horizScrollbarHeight;
4579         }
4580         if ( direction === 'rtl' ) {
4581                 if ( newPos.left !== '' ) {
4582                         newPos.left -= vertScrollbarWidth;
4583                 }
4584         } else {
4585                 if ( newPos.right !== '' ) {
4586                         newPos.right -= vertScrollbarWidth;
4587                 }
4588         }
4589
4590         return newPos;
4591 };
4592
4593 /**
4594  * Element that can be automatically clipped to visible boundaries.
4595  *
4596  * Whenever the element's natural height changes, you have to call
4597  * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4598  * clipping correctly.
4599  *
4600  * The dimensions of #$clippableContainer will be compared to the boundaries of the
4601  * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4602  * then #$clippable will be given a fixed reduced height and/or width and will be made
4603  * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4604  * but you can build a static footer by setting #$clippableContainer to an element that contains
4605  * #$clippable and the footer.
4606  *
4607  * @abstract
4608  * @class
4609  *
4610  * @constructor
4611  * @param {Object} [config] Configuration options
4612  * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4613  * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4614  *   omit to use #$clippable
4615  */
4616 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
4617         // Configuration initialization
4618         config = config || {};
4619
4620         // Properties
4621         this.$clippable = null;
4622         this.$clippableContainer = null;
4623         this.clipping = false;
4624         this.clippedHorizontally = false;
4625         this.clippedVertically = false;
4626         this.$clippableScrollableContainer = null;
4627         this.$clippableScroller = null;
4628         this.$clippableWindow = null;
4629         this.idealWidth = null;
4630         this.idealHeight = null;
4631         this.onClippableScrollHandler = this.clip.bind( this );
4632         this.onClippableWindowResizeHandler = this.clip.bind( this );
4633
4634         // Initialization
4635         if ( config.$clippableContainer ) {
4636                 this.setClippableContainer( config.$clippableContainer );
4637         }
4638         this.setClippableElement( config.$clippable || this.$element );
4639 };
4640
4641 /* Methods */
4642
4643 /**
4644  * Set clippable element.
4645  *
4646  * If an element is already set, it will be cleaned up before setting up the new element.
4647  *
4648  * @param {jQuery} $clippable Element to make clippable
4649  */
4650 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
4651         if ( this.$clippable ) {
4652                 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
4653                 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
4654                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
4655         }
4656
4657         this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
4658         this.clip();
4659 };
4660
4661 /**
4662  * Set clippable container.
4663  *
4664  * This is the container that will be measured when deciding whether to clip. When clipping,
4665  * #$clippable will be resized in order to keep the clippable container fully visible.
4666  *
4667  * If the clippable container is unset, #$clippable will be used.
4668  *
4669  * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4670  */
4671 OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
4672         this.$clippableContainer = $clippableContainer;
4673         if ( this.$clippable ) {
4674                 this.clip();
4675         }
4676 };
4677
4678 /**
4679  * Toggle clipping.
4680  *
4681  * Do not turn clipping on until after the element is attached to the DOM and visible.
4682  *
4683  * @param {boolean} [clipping] Enable clipping, omit to toggle
4684  * @chainable
4685  */
4686 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
4687         clipping = clipping === undefined ? !this.clipping : !!clipping;
4688
4689         if ( clipping && !this.warnedUnattached && !this.isElementAttached() ) {
4690                 OO.ui.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4691                 this.warnedUnattached = true;
4692         }
4693
4694         if ( this.clipping !== clipping ) {
4695                 this.clipping = clipping;
4696                 if ( clipping ) {
4697                         this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
4698                         // If the clippable container is the root, we have to listen to scroll events and check
4699                         // jQuery.scrollTop on the window because of browser inconsistencies
4700                         this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
4701                                 $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
4702                                 this.$clippableScrollableContainer;
4703                         this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
4704                         this.$clippableWindow = $( this.getElementWindow() )
4705                                 .on( 'resize', this.onClippableWindowResizeHandler );
4706                         // Initial clip after visible
4707                         this.clip();
4708                 } else {
4709                         this.$clippable.css( {
4710                                 width: '',
4711                                 height: '',
4712                                 maxWidth: '',
4713                                 maxHeight: '',
4714                                 overflowX: '',
4715                                 overflowY: ''
4716                         } );
4717                         OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
4718
4719                         this.$clippableScrollableContainer = null;
4720                         this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
4721                         this.$clippableScroller = null;
4722                         this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
4723                         this.$clippableWindow = null;
4724                 }
4725         }
4726
4727         return this;
4728 };
4729
4730 /**
4731  * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4732  *
4733  * @return {boolean} Element will be clipped to the visible area
4734  */
4735 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
4736         return this.clipping;
4737 };
4738
4739 /**
4740  * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4741  *
4742  * @return {boolean} Part of the element is being clipped
4743  */
4744 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
4745         return this.clippedHorizontally || this.clippedVertically;
4746 };
4747
4748 /**
4749  * Check if the right of the element is being clipped by the nearest scrollable container.
4750  *
4751  * @return {boolean} Part of the element is being clipped
4752  */
4753 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
4754         return this.clippedHorizontally;
4755 };
4756
4757 /**
4758  * Check if the bottom of the element is being clipped by the nearest scrollable container.
4759  *
4760  * @return {boolean} Part of the element is being clipped
4761  */
4762 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
4763         return this.clippedVertically;
4764 };
4765
4766 /**
4767  * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4768  *
4769  * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4770  * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4771  */
4772 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
4773         this.idealWidth = width;
4774         this.idealHeight = height;
4775
4776         if ( !this.clipping ) {
4777                 // Update dimensions
4778                 this.$clippable.css( { width: width, height: height } );
4779         }
4780         // While clipping, idealWidth and idealHeight are not considered
4781 };
4782
4783 /**
4784  * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4785  * when the element's natural height changes.
4786  *
4787  * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4788  * overlapped by, the visible area of the nearest scrollable container.
4789  *
4790  * Because calling clip() when the natural height changes isn't always possible, we also set
4791  * max-height when the element isn't being clipped. This means that if the element tries to grow
4792  * beyond the edge, something reasonable will happen before clip() is called.
4793  *
4794  * @chainable
4795  */
4796 OO.ui.mixin.ClippableElement.prototype.clip = function () {
4797         var $container, extraHeight, extraWidth, ccOffset,
4798                 $scrollableContainer, scOffset, scHeight, scWidth,
4799                 ccWidth, scrollerIsWindow, scrollTop, scrollLeft,
4800                 desiredWidth, desiredHeight, allotedWidth, allotedHeight,
4801                 naturalWidth, naturalHeight, clipWidth, clipHeight,
4802                 buffer = 7; // Chosen by fair dice roll
4803
4804         if ( !this.clipping ) {
4805                 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4806                 return this;
4807         }
4808
4809         $container = this.$clippableContainer || this.$clippable;
4810         extraHeight = $container.outerHeight() - this.$clippable.outerHeight();
4811         extraWidth = $container.outerWidth() - this.$clippable.outerWidth();
4812         ccOffset = $container.offset();
4813         if ( this.$clippableScrollableContainer.is( 'html, body' ) ) {
4814                 $scrollableContainer = this.$clippableWindow;
4815                 scOffset = { top: 0, left: 0 };
4816         } else {
4817                 $scrollableContainer = this.$clippableScrollableContainer;
4818                 scOffset = $scrollableContainer.offset();
4819         }
4820         scHeight = $scrollableContainer.innerHeight() - buffer;
4821         scWidth = $scrollableContainer.innerWidth() - buffer;
4822         ccWidth = $container.outerWidth() + buffer;
4823         scrollerIsWindow = this.$clippableScroller[ 0 ] === this.$clippableWindow[ 0 ];
4824         scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0;
4825         scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0;
4826         desiredWidth = ccOffset.left < 0 ?
4827                 ccWidth + ccOffset.left :
4828                 ( scOffset.left + scrollLeft + scWidth ) - ccOffset.left;
4829         desiredHeight = ( scOffset.top + scrollTop + scHeight ) - ccOffset.top;
4830         // It should never be desirable to exceed the dimensions of the browser viewport... right?
4831         desiredWidth = Math.min( desiredWidth, document.documentElement.clientWidth );
4832         desiredHeight = Math.min( desiredHeight, document.documentElement.clientHeight );
4833         allotedWidth = Math.ceil( desiredWidth - extraWidth );
4834         allotedHeight = Math.ceil( desiredHeight - extraHeight );
4835         naturalWidth = this.$clippable.prop( 'scrollWidth' );
4836         naturalHeight = this.$clippable.prop( 'scrollHeight' );
4837         clipWidth = allotedWidth < naturalWidth;
4838         clipHeight = allotedHeight < naturalHeight;
4839
4840         if ( clipWidth ) {
4841                 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
4842                 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4843                 this.$clippable.css( 'overflowX', 'scroll' );
4844                 void this.$clippable[ 0 ].offsetHeight; // Force reflow
4845                 this.$clippable.css( {
4846                         width: Math.max( 0, allotedWidth ),
4847                         maxWidth: ''
4848                 } );
4849         } else {
4850                 this.$clippable.css( {
4851                         overflowX: '',
4852                         width: this.idealWidth || '',
4853                         maxWidth: Math.max( 0, allotedWidth )
4854                 } );
4855         }
4856         if ( clipHeight ) {
4857                 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
4858                 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
4859                 this.$clippable.css( 'overflowY', 'scroll' );
4860                 void this.$clippable[ 0 ].offsetHeight; // Force reflow
4861                 this.$clippable.css( {
4862                         height: Math.max( 0, allotedHeight ),
4863                         maxHeight: ''
4864                 } );
4865         } else {
4866                 this.$clippable.css( {
4867                         overflowY: '',
4868                         height: this.idealHeight || '',
4869                         maxHeight: Math.max( 0, allotedHeight )
4870                 } );
4871         }
4872
4873         // If we stopped clipping in at least one of the dimensions
4874         if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
4875                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
4876         }
4877
4878         this.clippedHorizontally = clipWidth;
4879         this.clippedVertically = clipHeight;
4880
4881         return this;
4882 };
4883
4884 /**
4885  * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4886  * By default, each popup has an anchor that points toward its origin.
4887  * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4888  *
4889  * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
4890  *
4891  *     @example
4892  *     // A popup widget.
4893  *     var popup = new OO.ui.PopupWidget( {
4894  *         $content: $( '<p>Hi there!</p>' ),
4895  *         padded: true,
4896  *         width: 300
4897  *     } );
4898  *
4899  *     $( 'body' ).append( popup.$element );
4900  *     // To display the popup, toggle the visibility to 'true'.
4901  *     popup.toggle( true );
4902  *
4903  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4904  *
4905  * @class
4906  * @extends OO.ui.Widget
4907  * @mixins OO.ui.mixin.LabelElement
4908  * @mixins OO.ui.mixin.ClippableElement
4909  * @mixins OO.ui.mixin.FloatableElement
4910  *
4911  * @constructor
4912  * @param {Object} [config] Configuration options
4913  * @cfg {number} [width=320] Width of popup in pixels
4914  * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4915  * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4916  * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
4917  *  'above': Put popup above $floatableContainer; anchor points down to the horizontal center
4918  *           of $floatableContainer
4919  *  'below': Put popup below $floatableContainer; anchor points up to the horizontal center
4920  *           of $floatableContainer
4921  *  'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
4922  *            endwards (right/left) to the vertical center of $floatableContainer
4923  *  'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
4924  *            startwards (left/right) to the vertical center of $floatableContainer
4925  * @cfg {string} [align='center'] How to align the popup to $floatableContainer
4926  *  'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
4927  *              as possible while still keeping the anchor within the popup;
4928  *              if position is before/after, move the popup as far downwards as possible.
4929  *  'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
4930  *               as possible while still keeping the anchor within the popup;
4931  *               if position in before/after, move the popup as far upwards as possible.
4932  *  'center': Horizontally (if position is above/below) or vertically (before/after) align the center
4933  *            of the popup with the center of $floatableContainer.
4934  * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
4935  * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
4936  * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4937  *  See the [OOjs UI docs on MediaWiki][3] for an example.
4938  *  [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4939  * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4940  * @cfg {jQuery} [$content] Content to append to the popup's body
4941  * @cfg {jQuery} [$footer] Content to append to the popup's footer
4942  * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4943  * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4944  *  This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4945  *  for an example.
4946  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4947  * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
4948  *  button.
4949  * @cfg {boolean} [padded=false] Add padding to the popup's body
4950  */
4951 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
4952         // Configuration initialization
4953         config = config || {};
4954
4955         // Parent constructor
4956         OO.ui.PopupWidget.parent.call( this, config );
4957
4958         // Properties (must be set before ClippableElement constructor call)
4959         this.$body = $( '<div>' );
4960         this.$popup = $( '<div>' );
4961
4962         // Mixin constructors
4963         OO.ui.mixin.LabelElement.call( this, config );
4964         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, {
4965                 $clippable: this.$body,
4966                 $clippableContainer: this.$popup
4967         } ) );
4968         OO.ui.mixin.FloatableElement.call( this, config );
4969
4970         // Properties
4971         this.$anchor = $( '<div>' );
4972         // If undefined, will be computed lazily in computePosition()
4973         this.$container = config.$container;
4974         this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
4975         this.autoClose = !!config.autoClose;
4976         this.$autoCloseIgnore = config.$autoCloseIgnore;
4977         this.transitionTimeout = null;
4978         this.anchored = false;
4979         this.width = config.width !== undefined ? config.width : 320;
4980         this.height = config.height !== undefined ? config.height : null;
4981         this.onMouseDownHandler = this.onMouseDown.bind( this );
4982         this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
4983
4984         // Initialization
4985         this.toggleAnchor( config.anchor === undefined || config.anchor );
4986         this.setAlignment( config.align || 'center' );
4987         this.setPosition( config.position || 'below' );
4988         this.$body.addClass( 'oo-ui-popupWidget-body' );
4989         this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
4990         this.$popup
4991                 .addClass( 'oo-ui-popupWidget-popup' )
4992                 .append( this.$body );
4993         this.$element
4994                 .addClass( 'oo-ui-popupWidget' )
4995                 .append( this.$popup, this.$anchor );
4996         // Move content, which was added to #$element by OO.ui.Widget, to the body
4997         // FIXME This is gross, we should use '$body' or something for the config
4998         if ( config.$content instanceof jQuery ) {
4999                 this.$body.append( config.$content );
5000         }
5001
5002         if ( config.padded ) {
5003                 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
5004         }
5005
5006         if ( config.head ) {
5007                 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
5008                 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
5009                 this.$head = $( '<div>' )
5010                         .addClass( 'oo-ui-popupWidget-head' )
5011                         .append( this.$label, this.closeButton.$element );
5012                 this.$popup.prepend( this.$head );
5013         }
5014
5015         if ( config.$footer ) {
5016                 this.$footer = $( '<div>' )
5017                         .addClass( 'oo-ui-popupWidget-footer' )
5018                         .append( config.$footer );
5019                 this.$popup.append( this.$footer );
5020         }
5021
5022         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5023         // that reference properties not initialized at that time of parent class construction
5024         // TODO: Find a better way to handle post-constructor setup
5025         this.visible = false;
5026         this.$element.addClass( 'oo-ui-element-hidden' );
5027 };
5028
5029 /* Setup */
5030
5031 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
5032 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
5033 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
5034 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.FloatableElement );
5035
5036 /* Events */
5037
5038 /**
5039  * @event ready
5040  *
5041  * The popup is ready: it is visible and has been positioned and clipped.
5042  */
5043
5044 /* Methods */
5045
5046 /**
5047  * Handles mouse down events.
5048  *
5049  * @private
5050  * @param {MouseEvent} e Mouse down event
5051  */
5052 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
5053         if (
5054                 this.isVisible() &&
5055                 !OO.ui.contains( this.$element.add( this.$autoCloseIgnore ).get(), e.target, true )
5056         ) {
5057                 this.toggle( false );
5058         }
5059 };
5060
5061 /**
5062  * Bind mouse down listener.
5063  *
5064  * @private
5065  */
5066 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
5067         // Capture clicks outside popup
5068         this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
5069 };
5070
5071 /**
5072  * Handles close button click events.
5073  *
5074  * @private
5075  */
5076 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
5077         if ( this.isVisible() ) {
5078                 this.toggle( false );
5079         }
5080 };
5081
5082 /**
5083  * Unbind mouse down listener.
5084  *
5085  * @private
5086  */
5087 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
5088         this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
5089 };
5090
5091 /**
5092  * Handles key down events.
5093  *
5094  * @private
5095  * @param {KeyboardEvent} e Key down event
5096  */
5097 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
5098         if (
5099                 e.which === OO.ui.Keys.ESCAPE &&
5100                 this.isVisible()
5101         ) {
5102                 this.toggle( false );
5103                 e.preventDefault();
5104                 e.stopPropagation();
5105         }
5106 };
5107
5108 /**
5109  * Bind key down listener.
5110  *
5111  * @private
5112  */
5113 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
5114         this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
5115 };
5116
5117 /**
5118  * Unbind key down listener.
5119  *
5120  * @private
5121  */
5122 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
5123         this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
5124 };
5125
5126 /**
5127  * Show, hide, or toggle the visibility of the anchor.
5128  *
5129  * @param {boolean} [show] Show anchor, omit to toggle
5130  */
5131 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
5132         show = show === undefined ? !this.anchored : !!show;
5133
5134         if ( this.anchored !== show ) {
5135                 if ( show ) {
5136                         this.$element.addClass( 'oo-ui-popupWidget-anchored' );
5137                         this.$element.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge );
5138                 } else {
5139                         this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
5140                         this.$element.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge );
5141                 }
5142                 this.anchored = show;
5143         }
5144 };
5145 /**
5146  * Change which edge the anchor appears on.
5147  *
5148  * @param {string} edge 'top', 'bottom', 'start' or 'end'
5149  */
5150 OO.ui.PopupWidget.prototype.setAnchorEdge = function ( edge ) {
5151         if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge ) === -1 ) {
5152                 throw new Error( 'Invalid value for edge: ' + edge );
5153         }
5154         if ( this.anchorEdge !== null ) {
5155                 this.$element.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge );
5156         }
5157         this.anchorEdge = edge;
5158         if ( this.anchored ) {
5159                 this.$element.addClass( 'oo-ui-popupWidget-anchored-' + edge );
5160         }
5161 };
5162
5163 /**
5164  * Check if the anchor is visible.
5165  *
5166  * @return {boolean} Anchor is visible
5167  */
5168 OO.ui.PopupWidget.prototype.hasAnchor = function () {
5169         return this.anchored;
5170 };
5171
5172 /**
5173  * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5174  * `.toggle( true )` after its #$element is attached to the DOM.
5175  *
5176  * Do not show the popup while it is not attached to the DOM. The calculations required to display
5177  * it in the right place and with the right dimensions only work correctly while it is attached.
5178  * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5179  * strictly enforced, so currently it only generates a warning in the browser console.
5180  *
5181  * @fires ready
5182  * @inheritdoc
5183  */
5184 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
5185         var change;
5186         show = show === undefined ? !this.isVisible() : !!show;
5187
5188         change = show !== this.isVisible();
5189
5190         if ( show && !this.warnedUnattached && !this.isElementAttached() ) {
5191                 OO.ui.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5192                 this.warnedUnattached = true;
5193         }
5194         if ( show && !this.$floatableContainer && this.isElementAttached() ) {
5195                 // Fall back to the parent node if the floatableContainer is not set
5196                 this.setFloatableContainer( this.$element.parent() );
5197         }
5198
5199         // Parent method
5200         OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
5201
5202         if ( change ) {
5203                 this.togglePositioning( show && !!this.$floatableContainer );
5204
5205                 if ( show ) {
5206                         if ( this.autoClose ) {
5207                                 this.bindMouseDownListener();
5208                                 this.bindKeyDownListener();
5209                         }
5210                         this.updateDimensions();
5211                         this.toggleClipping( true );
5212                         this.emit( 'ready' );
5213                 } else {
5214                         this.toggleClipping( false );
5215                         if ( this.autoClose ) {
5216                                 this.unbindMouseDownListener();
5217                                 this.unbindKeyDownListener();
5218                         }
5219                 }
5220         }
5221
5222         return this;
5223 };
5224
5225 /**
5226  * Set the size of the popup.
5227  *
5228  * Changing the size may also change the popup's position depending on the alignment.
5229  *
5230  * @param {number} width Width in pixels
5231  * @param {number} height Height in pixels
5232  * @param {boolean} [transition=false] Use a smooth transition
5233  * @chainable
5234  */
5235 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
5236         this.width = width;
5237         this.height = height !== undefined ? height : null;
5238         if ( this.isVisible() ) {
5239                 this.updateDimensions( transition );
5240         }
5241 };
5242
5243 /**
5244  * Update the size and position.
5245  *
5246  * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5247  * be called automatically.
5248  *
5249  * @param {boolean} [transition=false] Use a smooth transition
5250  * @chainable
5251  */
5252 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
5253         var widget = this;
5254
5255         // Prevent transition from being interrupted
5256         clearTimeout( this.transitionTimeout );
5257         if ( transition ) {
5258                 // Enable transition
5259                 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
5260         }
5261
5262         this.position();
5263
5264         if ( transition ) {
5265                 // Prevent transitioning after transition is complete
5266                 this.transitionTimeout = setTimeout( function () {
5267                         widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
5268                 }, 200 );
5269         } else {
5270                 // Prevent transitioning immediately
5271                 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
5272         }
5273 };
5274
5275 /**
5276  * @inheritdoc
5277  */
5278 OO.ui.PopupWidget.prototype.computePosition = function () {
5279         var direction, align, vertical, start, end, near, far, sizeProp, popupSize, anchorSize, anchorPos,
5280                 anchorOffset, anchorMargin, parentPosition, positionProp, positionAdjustment, floatablePos,
5281                 offsetParentPos, containerPos,
5282                 popupPos = {},
5283                 anchorCss = { left: '', right: '', top: '', bottom: '' },
5284                 alignMap = {
5285                         ltr: {
5286                                 'force-left': 'backwards',
5287                                 'force-right': 'forwards'
5288                         },
5289                         rtl: {
5290                                 'force-left': 'forwards',
5291                                 'force-right': 'backwards'
5292                         }
5293                 },
5294                 anchorEdgeMap = {
5295                         above: 'bottom',
5296                         below: 'top',
5297                         before: 'end',
5298                         after: 'start'
5299                 },
5300                 hPosMap = {
5301                         forwards: 'start',
5302                         center: 'center',
5303                         backwards: this.anchored ? 'before' : 'end'
5304                 },
5305                 vPosMap = {
5306                         forwards: 'top',
5307                         center: 'center',
5308                         backwards: 'bottom'
5309                 };
5310
5311         if ( !this.$container ) {
5312                 // Lazy-initialize $container if not specified in constructor
5313                 this.$container = $( this.getClosestScrollableElementContainer() );
5314         }
5315         direction = this.$container.css( 'direction' );
5316
5317         // Set height and width before we do anything else, since it might cause our measurements
5318         // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5319         this.$popup.css( {
5320                 width: this.width,
5321                 height: this.height !== null ? this.height : 'auto'
5322         } );
5323
5324         align = alignMap[ direction ][ this.align ] || this.align;
5325         // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5326         vertical = this.popupPosition === 'before' || this.popupPosition === 'after';
5327         start = vertical ? 'top' : ( direction === 'rtl' ? 'right' : 'left' );
5328         end = vertical ? 'bottom' : ( direction === 'rtl' ? 'left' : 'right' );
5329         near = vertical ? 'top' : 'left';
5330         far = vertical ? 'bottom' : 'right';
5331         sizeProp = vertical ? 'Height' : 'Width';
5332         popupSize = vertical ? ( this.height || this.$popup.height() ) : this.width;
5333
5334         this.setAnchorEdge( anchorEdgeMap[ this.popupPosition ] );
5335         this.horizontalPosition = vertical ? this.popupPosition : hPosMap[ align ];
5336         this.verticalPosition = vertical ? vPosMap[ align ] : this.popupPosition;
5337
5338         // Parent method
5339         parentPosition = OO.ui.mixin.FloatableElement.prototype.computePosition.call( this );
5340         // Find out which property FloatableElement used for positioning, and adjust that value
5341         positionProp = vertical ?
5342                 ( parentPosition.top !== '' ? 'top' : 'bottom' ) :
5343                 ( parentPosition.left !== '' ? 'left' : 'right' );
5344
5345         // Figure out where the near and far edges of the popup and $floatableContainer are
5346         floatablePos = this.$floatableContainer.offset();
5347         floatablePos[ far ] = floatablePos[ near ] + this.$floatableContainer[ 'outer' + sizeProp ]();
5348         // Measure where the offsetParent is and compute our position based on that and parentPosition
5349         offsetParentPos = this.$element.offsetParent().offset();
5350
5351         if ( positionProp === near ) {
5352                 popupPos[ near ] = offsetParentPos[ near ] + parentPosition[ near ];
5353                 popupPos[ far ] = popupPos[ near ] + popupSize;
5354         } else {
5355                 popupPos[ far ] = offsetParentPos[ near ] +
5356                         this.$element.offsetParent()[ 'inner' + sizeProp ]() - parentPosition[ far ];
5357                 popupPos[ near ] = popupPos[ far ] - popupSize;
5358         }
5359
5360         if ( this.anchored ) {
5361                 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5362                 anchorPos = ( floatablePos[ start ] + floatablePos[ end ] ) / 2;
5363                 anchorOffset = ( start === far ? -1 : 1 ) * ( anchorPos - popupPos[ start ] );
5364
5365                 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5366                 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5367                 anchorSize = this.$anchor[ 0 ][ 'scroll' + sizeProp ];
5368                 anchorMargin = parseFloat( this.$anchor.css( 'margin-' + start ) );
5369                 if ( anchorOffset + anchorMargin < 2 * anchorSize ) {
5370                         // Not enough space for the anchor on the start side; pull the popup startwards
5371                         positionAdjustment = ( positionProp === start ? -1 : 1 ) *
5372                                 ( 2 * anchorSize - ( anchorOffset + anchorMargin ) );
5373                 } else if ( anchorOffset + anchorMargin > popupSize - 2 * anchorSize ) {
5374                         // Not enough space for the anchor on the end side; pull the popup endwards
5375                         positionAdjustment = ( positionProp === end ? -1 : 1 ) *
5376                                 ( anchorOffset + anchorMargin - ( popupSize - 2 * anchorSize ) );
5377                 } else {
5378                         positionAdjustment = 0;
5379                 }
5380         } else {
5381                 positionAdjustment = 0;
5382         }
5383
5384         // Check if the popup will go beyond the edge of this.$container
5385         containerPos = this.$container.offset();
5386         containerPos[ far ] = containerPos[ near ] + this.$container[ 'inner' + sizeProp ]();
5387         // Take into account how much the popup will move because of the adjustments we're going to make
5388         popupPos[ near ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment;
5389         popupPos[ far ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment;
5390         if ( containerPos[ near ] + this.containerPadding > popupPos[ near ] ) {
5391                 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5392                 positionAdjustment += ( positionProp === near ? 1 : -1 ) *
5393                         ( containerPos[ near ] + this.containerPadding - popupPos[ near ] );
5394         } else if ( containerPos[ far ] - this.containerPadding < popupPos[ far ] ) {
5395                 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5396                 positionAdjustment += ( positionProp === far ? 1 : -1 ) *
5397                         ( popupPos[ far ] - ( containerPos[ far ] - this.containerPadding ) );
5398         }
5399
5400         if ( this.anchored ) {
5401                 // Adjust anchorOffset for positionAdjustment
5402                 anchorOffset += ( positionProp === start ? -1 : 1 ) * positionAdjustment;
5403
5404                 // Position the anchor
5405                 anchorCss[ start ] = anchorOffset;
5406                 this.$anchor.css( anchorCss );
5407         }
5408
5409         // Move the popup if needed
5410         parentPosition[ positionProp ] += positionAdjustment;
5411
5412         return parentPosition;
5413 };
5414
5415 /**
5416  * Set popup alignment
5417  *
5418  * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5419  *  `backwards` or `forwards`.
5420  */
5421 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
5422         // Validate alignment
5423         if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
5424                 this.align = align;
5425         } else {
5426                 this.align = 'center';
5427         }
5428         this.position();
5429 };
5430
5431 /**
5432  * Get popup alignment
5433  *
5434  * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5435  *  `backwards` or `forwards`.
5436  */
5437 OO.ui.PopupWidget.prototype.getAlignment = function () {
5438         return this.align;
5439 };
5440
5441 /**
5442  * Change the positioning of the popup.
5443  *
5444  * @param {string} position 'above', 'below', 'before' or 'after'
5445  */
5446 OO.ui.PopupWidget.prototype.setPosition = function ( position ) {
5447         if ( [ 'above', 'below', 'before', 'after' ].indexOf( position ) === -1 ) {
5448                 position = 'below';
5449         }
5450         this.popupPosition = position;
5451         this.position();
5452 };
5453
5454 /**
5455  * Get popup positioning.
5456  *
5457  * @return {string} 'above', 'below', 'before' or 'after'
5458  */
5459 OO.ui.PopupWidget.prototype.getPosition = function () {
5460         return this.popupPosition;
5461 };
5462
5463 /**
5464  * Get an ID of the body element, this can be used as the
5465  * `aria-describedby` attribute for an input field.
5466  *
5467  * @return {string} The ID of the body element
5468  */
5469 OO.ui.PopupWidget.prototype.getBodyId = function () {
5470         var id = this.$body.attr( 'id' );
5471         if ( id === undefined ) {
5472                 id = OO.ui.generateElementId();
5473                 this.$body.attr( 'id', id );
5474         }
5475         return id;
5476 };
5477
5478 /**
5479  * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5480  * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5481  * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5482  * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5483  *
5484  * @abstract
5485  * @class
5486  *
5487  * @constructor
5488  * @param {Object} [config] Configuration options
5489  * @cfg {Object} [popup] Configuration to pass to popup
5490  * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5491  */
5492 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
5493         // Configuration initialization
5494         config = config || {};
5495
5496         // Properties
5497         this.popup = new OO.ui.PopupWidget( $.extend(
5498                 {
5499                         autoClose: true,
5500                         $floatableContainer: this.$element
5501                 },
5502                 config.popup,
5503                 {
5504                         $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore )
5505                 }
5506         ) );
5507 };
5508
5509 /* Methods */
5510
5511 /**
5512  * Get popup.
5513  *
5514  * @return {OO.ui.PopupWidget} Popup widget
5515  */
5516 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
5517         return this.popup;
5518 };
5519
5520 /**
5521  * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5522  * which is used to display additional information or options.
5523  *
5524  *     @example
5525  *     // Example of a popup button.
5526  *     var popupButton = new OO.ui.PopupButtonWidget( {
5527  *         label: 'Popup button with options',
5528  *         icon: 'menu',
5529  *         popup: {
5530  *             $content: $( '<p>Additional options here.</p>' ),
5531  *             padded: true,
5532  *             align: 'force-left'
5533  *         }
5534  *     } );
5535  *     // Append the button to the DOM.
5536  *     $( 'body' ).append( popupButton.$element );
5537  *
5538  * @class
5539  * @extends OO.ui.ButtonWidget
5540  * @mixins OO.ui.mixin.PopupElement
5541  *
5542  * @constructor
5543  * @param {Object} [config] Configuration options
5544  * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
5545  *  the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
5546  *  containing `<div>` and has a larger area. By default, the popup uses relative positioning.
5547  *  See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
5548  */
5549 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
5550         // Configuration initialization
5551         config = config || {};
5552
5553         // Parent constructor
5554         OO.ui.PopupButtonWidget.parent.call( this, config );
5555
5556         // Mixin constructors
5557         OO.ui.mixin.PopupElement.call( this, config );
5558
5559         // Properties
5560         this.$overlay = config.$overlay || this.$element;
5561
5562         // Events
5563         this.connect( this, { click: 'onAction' } );
5564
5565         // Initialization
5566         this.$element
5567                 .addClass( 'oo-ui-popupButtonWidget' )
5568                 .attr( 'aria-haspopup', 'true' );
5569         this.popup.$element
5570                 .addClass( 'oo-ui-popupButtonWidget-popup' )
5571                 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
5572                 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
5573         this.$overlay.append( this.popup.$element );
5574 };
5575
5576 /* Setup */
5577
5578 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
5579 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
5580
5581 /* Methods */
5582
5583 /**
5584  * Handle the button action being triggered.
5585  *
5586  * @private
5587  */
5588 OO.ui.PopupButtonWidget.prototype.onAction = function () {
5589         this.popup.toggle();
5590 };
5591
5592 /**
5593  * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
5594  *
5595  * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
5596  *
5597  * @private
5598  * @abstract
5599  * @class
5600  * @mixins OO.ui.mixin.GroupElement
5601  *
5602  * @constructor
5603  * @param {Object} [config] Configuration options
5604  */
5605 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
5606         // Mixin constructors
5607         OO.ui.mixin.GroupElement.call( this, config );
5608 };
5609
5610 /* Setup */
5611
5612 OO.mixinClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
5613
5614 /* Methods */
5615
5616 /**
5617  * Set the disabled state of the widget.
5618  *
5619  * This will also update the disabled state of child widgets.
5620  *
5621  * @param {boolean} disabled Disable widget
5622  * @chainable
5623  */
5624 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
5625         var i, len;
5626
5627         // Parent method
5628         // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
5629         OO.ui.Widget.prototype.setDisabled.call( this, disabled );
5630
5631         // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
5632         if ( this.items ) {
5633                 for ( i = 0, len = this.items.length; i < len; i++ ) {
5634                         this.items[ i ].updateDisabled();
5635                 }
5636         }
5637
5638         return this;
5639 };
5640
5641 /**
5642  * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
5643  *
5644  * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
5645  * allows bidirectional communication.
5646  *
5647  * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
5648  *
5649  * @private
5650  * @abstract
5651  * @class
5652  *
5653  * @constructor
5654  */
5655 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
5656         //
5657 };
5658
5659 /* Methods */
5660
5661 /**
5662  * Check if widget is disabled.
5663  *
5664  * Checks parent if present, making disabled state inheritable.
5665  *
5666  * @return {boolean} Widget is disabled
5667  */
5668 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
5669         return this.disabled ||
5670                 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
5671 };
5672
5673 /**
5674  * Set group element is in.
5675  *
5676  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
5677  * @chainable
5678  */
5679 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
5680         // Parent method
5681         // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
5682         OO.ui.Element.prototype.setElementGroup.call( this, group );
5683
5684         // Initialize item disabled states
5685         this.updateDisabled();
5686
5687         return this;
5688 };
5689
5690 /**
5691  * OptionWidgets are special elements that can be selected and configured with data. The
5692  * data is often unique for each option, but it does not have to be. OptionWidgets are used
5693  * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
5694  * and examples, please see the [OOjs UI documentation on MediaWiki][1].
5695  *
5696  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5697  *
5698  * @class
5699  * @extends OO.ui.Widget
5700  * @mixins OO.ui.mixin.ItemWidget
5701  * @mixins OO.ui.mixin.LabelElement
5702  * @mixins OO.ui.mixin.FlaggedElement
5703  * @mixins OO.ui.mixin.AccessKeyedElement
5704  *
5705  * @constructor
5706  * @param {Object} [config] Configuration options
5707  */
5708 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
5709         // Configuration initialization
5710         config = config || {};
5711
5712         // Parent constructor
5713         OO.ui.OptionWidget.parent.call( this, config );
5714
5715         // Mixin constructors
5716         OO.ui.mixin.ItemWidget.call( this );
5717         OO.ui.mixin.LabelElement.call( this, config );
5718         OO.ui.mixin.FlaggedElement.call( this, config );
5719         OO.ui.mixin.AccessKeyedElement.call( this, config );
5720
5721         // Properties
5722         this.selected = false;
5723         this.highlighted = false;
5724         this.pressed = false;
5725
5726         // Initialization
5727         this.$element
5728                 .data( 'oo-ui-optionWidget', this )
5729                 // Allow programmatic focussing (and by accesskey), but not tabbing
5730                 .attr( 'tabindex', '-1' )
5731                 .attr( 'role', 'option' )
5732                 .attr( 'aria-selected', 'false' )
5733                 .addClass( 'oo-ui-optionWidget' )
5734                 .append( this.$label );
5735 };
5736
5737 /* Setup */
5738
5739 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
5740 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
5741 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
5742 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
5743 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.AccessKeyedElement );
5744
5745 /* Static Properties */
5746
5747 /**
5748  * Whether this option can be selected. See #setSelected.
5749  *
5750  * @static
5751  * @inheritable
5752  * @property {boolean}
5753  */
5754 OO.ui.OptionWidget.static.selectable = true;
5755
5756 /**
5757  * Whether this option can be highlighted. See #setHighlighted.
5758  *
5759  * @static
5760  * @inheritable
5761  * @property {boolean}
5762  */
5763 OO.ui.OptionWidget.static.highlightable = true;
5764
5765 /**
5766  * Whether this option can be pressed. See #setPressed.
5767  *
5768  * @static
5769  * @inheritable
5770  * @property {boolean}
5771  */
5772 OO.ui.OptionWidget.static.pressable = true;
5773
5774 /**
5775  * Whether this option will be scrolled into view when it is selected.
5776  *
5777  * @static
5778  * @inheritable
5779  * @property {boolean}
5780  */
5781 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
5782
5783 /* Methods */
5784
5785 /**
5786  * Check if the option can be selected.
5787  *
5788  * @return {boolean} Item is selectable
5789  */
5790 OO.ui.OptionWidget.prototype.isSelectable = function () {
5791         return this.constructor.static.selectable && !this.isDisabled() && this.isVisible();
5792 };
5793
5794 /**
5795  * Check if the option can be highlighted. A highlight indicates that the option
5796  * may be selected when a user presses enter or clicks. Disabled items cannot
5797  * be highlighted.
5798  *
5799  * @return {boolean} Item is highlightable
5800  */
5801 OO.ui.OptionWidget.prototype.isHighlightable = function () {
5802         return this.constructor.static.highlightable && !this.isDisabled() && this.isVisible();
5803 };
5804
5805 /**
5806  * Check if the option can be pressed. The pressed state occurs when a user mouses
5807  * down on an item, but has not yet let go of the mouse.
5808  *
5809  * @return {boolean} Item is pressable
5810  */
5811 OO.ui.OptionWidget.prototype.isPressable = function () {
5812         return this.constructor.static.pressable && !this.isDisabled() && this.isVisible();
5813 };
5814
5815 /**
5816  * Check if the option is selected.
5817  *
5818  * @return {boolean} Item is selected
5819  */
5820 OO.ui.OptionWidget.prototype.isSelected = function () {
5821         return this.selected;
5822 };
5823
5824 /**
5825  * Check if the option is highlighted. A highlight indicates that the
5826  * item may be selected when a user presses enter or clicks.
5827  *
5828  * @return {boolean} Item is highlighted
5829  */
5830 OO.ui.OptionWidget.prototype.isHighlighted = function () {
5831         return this.highlighted;
5832 };
5833
5834 /**
5835  * Check if the option is pressed. The pressed state occurs when a user mouses
5836  * down on an item, but has not yet let go of the mouse. The item may appear
5837  * selected, but it will not be selected until the user releases the mouse.
5838  *
5839  * @return {boolean} Item is pressed
5840  */
5841 OO.ui.OptionWidget.prototype.isPressed = function () {
5842         return this.pressed;
5843 };
5844
5845 /**
5846  * Set the option’s selected state. In general, all modifications to the selection
5847  * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
5848  * method instead of this method.
5849  *
5850  * @param {boolean} [state=false] Select option
5851  * @chainable
5852  */
5853 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
5854         if ( this.constructor.static.selectable ) {
5855                 this.selected = !!state;
5856                 this.$element
5857                         .toggleClass( 'oo-ui-optionWidget-selected', state )
5858                         .attr( 'aria-selected', state.toString() );
5859                 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
5860                         this.scrollElementIntoView();
5861                 }
5862                 this.updateThemeClasses();
5863         }
5864         return this;
5865 };
5866
5867 /**
5868  * Set the option’s highlighted state. In general, all programmatic
5869  * modifications to the highlight should be handled by the
5870  * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
5871  * method instead of this method.
5872  *
5873  * @param {boolean} [state=false] Highlight option
5874  * @chainable
5875  */
5876 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
5877         if ( this.constructor.static.highlightable ) {
5878                 this.highlighted = !!state;
5879                 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
5880                 this.updateThemeClasses();
5881         }
5882         return this;
5883 };
5884
5885 /**
5886  * Set the option’s pressed state. In general, all
5887  * programmatic modifications to the pressed state should be handled by the
5888  * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
5889  * method instead of this method.
5890  *
5891  * @param {boolean} [state=false] Press option
5892  * @chainable
5893  */
5894 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
5895         if ( this.constructor.static.pressable ) {
5896                 this.pressed = !!state;
5897                 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
5898                 this.updateThemeClasses();
5899         }
5900         return this;
5901 };
5902
5903 /**
5904  * Get text to match search strings against.
5905  *
5906  * The default implementation returns the label text, but subclasses
5907  * can override this to provide more complex behavior.
5908  *
5909  * @return {string|boolean} String to match search string against
5910  */
5911 OO.ui.OptionWidget.prototype.getMatchText = function () {
5912         var label = this.getLabel();
5913         return typeof label === 'string' ? label : this.$label.text();
5914 };
5915
5916 /**
5917  * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
5918  * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
5919  * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
5920  * menu selects}.
5921  *
5922  * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
5923  * information, please see the [OOjs UI documentation on MediaWiki][1].
5924  *
5925  *     @example
5926  *     // Example of a select widget with three options
5927  *     var select = new OO.ui.SelectWidget( {
5928  *         items: [
5929  *             new OO.ui.OptionWidget( {
5930  *                 data: 'a',
5931  *                 label: 'Option One',
5932  *             } ),
5933  *             new OO.ui.OptionWidget( {
5934  *                 data: 'b',
5935  *                 label: 'Option Two',
5936  *             } ),
5937  *             new OO.ui.OptionWidget( {
5938  *                 data: 'c',
5939  *                 label: 'Option Three',
5940  *             } )
5941  *         ]
5942  *     } );
5943  *     $( 'body' ).append( select.$element );
5944  *
5945  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5946  *
5947  * @abstract
5948  * @class
5949  * @extends OO.ui.Widget
5950  * @mixins OO.ui.mixin.GroupWidget
5951  *
5952  * @constructor
5953  * @param {Object} [config] Configuration options
5954  * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
5955  *  Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
5956  *  the [OOjs UI documentation on MediaWiki] [2] for examples.
5957  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5958  */
5959 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
5960         // Configuration initialization
5961         config = config || {};
5962
5963         // Parent constructor
5964         OO.ui.SelectWidget.parent.call( this, config );
5965
5966         // Mixin constructors
5967         OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
5968
5969         // Properties
5970         this.pressed = false;
5971         this.selecting = null;
5972         this.onMouseUpHandler = this.onMouseUp.bind( this );
5973         this.onMouseMoveHandler = this.onMouseMove.bind( this );
5974         this.onKeyDownHandler = this.onKeyDown.bind( this );
5975         this.onKeyPressHandler = this.onKeyPress.bind( this );
5976         this.keyPressBuffer = '';
5977         this.keyPressBufferTimer = null;
5978         this.blockMouseOverEvents = 0;
5979
5980         // Events
5981         this.connect( this, {
5982                 toggle: 'onToggle'
5983         } );
5984         this.$element.on( {
5985                 focusin: this.onFocus.bind( this ),
5986                 mousedown: this.onMouseDown.bind( this ),
5987                 mouseover: this.onMouseOver.bind( this ),
5988                 mouseleave: this.onMouseLeave.bind( this )
5989         } );
5990
5991         // Initialization
5992         this.$element
5993                 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
5994                 .attr( 'role', 'listbox' );
5995         this.setFocusOwner( this.$element );
5996         if ( Array.isArray( config.items ) ) {
5997                 this.addItems( config.items );
5998         }
5999 };
6000
6001 /* Setup */
6002
6003 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
6004 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
6005
6006 /* Events */
6007
6008 /**
6009  * @event highlight
6010  *
6011  * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6012  *
6013  * @param {OO.ui.OptionWidget|null} item Highlighted item
6014  */
6015
6016 /**
6017  * @event press
6018  *
6019  * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6020  * pressed state of an option.
6021  *
6022  * @param {OO.ui.OptionWidget|null} item Pressed item
6023  */
6024
6025 /**
6026  * @event select
6027  *
6028  * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
6029  *
6030  * @param {OO.ui.OptionWidget|null} item Selected item
6031  */
6032
6033 /**
6034  * @event choose
6035  * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6036  * @param {OO.ui.OptionWidget} item Chosen item
6037  */
6038
6039 /**
6040  * @event add
6041  *
6042  * An `add` event is emitted when options are added to the select with the #addItems method.
6043  *
6044  * @param {OO.ui.OptionWidget[]} items Added items
6045  * @param {number} index Index of insertion point
6046  */
6047
6048 /**
6049  * @event remove
6050  *
6051  * A `remove` event is emitted when options are removed from the select with the #clearItems
6052  * or #removeItems methods.
6053  *
6054  * @param {OO.ui.OptionWidget[]} items Removed items
6055  */
6056
6057 /* Methods */
6058
6059 /**
6060  * Handle focus events
6061  *
6062  * @private
6063  * @param {jQuery.Event} event
6064  */
6065 OO.ui.SelectWidget.prototype.onFocus = function ( event ) {
6066         var item;
6067         if ( event.target === this.$element[ 0 ] ) {
6068                 // This widget was focussed, e.g. by the user tabbing to it.
6069                 // The styles for focus state depend on one of the items being selected.
6070                 if ( !this.getSelectedItem() ) {
6071                         item = this.getFirstSelectableItem();
6072                 }
6073         } else {
6074                 // One of the options got focussed (and the event bubbled up here).
6075                 // They can't be tabbed to, but they can be activated using accesskeys.
6076                 item = this.findTargetItem( event );
6077         }
6078
6079         if ( item ) {
6080                 if ( item.constructor.static.highlightable ) {
6081                         this.highlightItem( item );
6082                 } else {
6083                         this.selectItem( item );
6084                 }
6085         }
6086
6087         if ( event.target !== this.$element[ 0 ] ) {
6088                 this.$focusOwner.focus();
6089         }
6090 };
6091
6092 /**
6093  * Handle mouse down events.
6094  *
6095  * @private
6096  * @param {jQuery.Event} e Mouse down event
6097  */
6098 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
6099         var item;
6100
6101         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
6102                 this.togglePressed( true );
6103                 item = this.findTargetItem( e );
6104                 if ( item && item.isSelectable() ) {
6105                         this.pressItem( item );
6106                         this.selecting = item;
6107                         this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
6108                         this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler, true );
6109                 }
6110         }
6111         return false;
6112 };
6113
6114 /**
6115  * Handle mouse up events.
6116  *
6117  * @private
6118  * @param {MouseEvent} e Mouse up event
6119  */
6120 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
6121         var item;
6122
6123         this.togglePressed( false );
6124         if ( !this.selecting ) {
6125                 item = this.findTargetItem( e );
6126                 if ( item && item.isSelectable() ) {
6127                         this.selecting = item;
6128                 }
6129         }
6130         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT && this.selecting ) {
6131                 this.pressItem( null );
6132                 this.chooseItem( this.selecting );
6133                 this.selecting = null;
6134         }
6135
6136         this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
6137         this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler, true );
6138
6139         return false;
6140 };
6141
6142 /**
6143  * Handle mouse move events.
6144  *
6145  * @private
6146  * @param {MouseEvent} e Mouse move event
6147  */
6148 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
6149         var item;
6150
6151         if ( !this.isDisabled() && this.pressed ) {
6152                 item = this.findTargetItem( e );
6153                 if ( item && item !== this.selecting && item.isSelectable() ) {
6154                         this.pressItem( item );
6155                         this.selecting = item;
6156                 }
6157         }
6158 };
6159
6160 /**
6161  * Handle mouse over events.
6162  *
6163  * @private
6164  * @param {jQuery.Event} e Mouse over event
6165  */
6166 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
6167         var item;
6168         if ( this.blockMouseOverEvents ) {
6169                 return;
6170         }
6171         if ( !this.isDisabled() ) {
6172                 item = this.findTargetItem( e );
6173                 this.highlightItem( item && item.isHighlightable() ? item : null );
6174         }
6175         return false;
6176 };
6177
6178 /**
6179  * Handle mouse leave events.
6180  *
6181  * @private
6182  * @param {jQuery.Event} e Mouse over event
6183  */
6184 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
6185         if ( !this.isDisabled() ) {
6186                 this.highlightItem( null );
6187         }
6188         return false;
6189 };
6190
6191 /**
6192  * Handle key down events.
6193  *
6194  * @protected
6195  * @param {KeyboardEvent} e Key down event
6196  */
6197 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
6198         var nextItem,
6199                 handled = false,
6200                 currentItem = this.getHighlightedItem() || this.getSelectedItem();
6201
6202         if ( !this.isDisabled() && this.isVisible() ) {
6203                 switch ( e.keyCode ) {
6204                         case OO.ui.Keys.ENTER:
6205                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
6206                                         // Was only highlighted, now let's select it. No-op if already selected.
6207                                         this.chooseItem( currentItem );
6208                                         handled = true;
6209                                 }
6210                                 break;
6211                         case OO.ui.Keys.UP:
6212                         case OO.ui.Keys.LEFT:
6213                                 this.clearKeyPressBuffer();
6214                                 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
6215                                 handled = true;
6216                                 break;
6217                         case OO.ui.Keys.DOWN:
6218                         case OO.ui.Keys.RIGHT:
6219                                 this.clearKeyPressBuffer();
6220                                 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
6221                                 handled = true;
6222                                 break;
6223                         case OO.ui.Keys.ESCAPE:
6224                         case OO.ui.Keys.TAB:
6225                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
6226                                         currentItem.setHighlighted( false );
6227                                 }
6228                                 this.unbindKeyDownListener();
6229                                 this.unbindKeyPressListener();
6230                                 // Don't prevent tabbing away / defocusing
6231                                 handled = false;
6232                                 break;
6233                 }
6234
6235                 if ( nextItem ) {
6236                         if ( nextItem.constructor.static.highlightable ) {
6237                                 this.highlightItem( nextItem );
6238                         } else {
6239                                 this.chooseItem( nextItem );
6240                         }
6241                         this.scrollItemIntoView( nextItem );
6242                 }
6243
6244                 if ( handled ) {
6245                         e.preventDefault();
6246                         e.stopPropagation();
6247                 }
6248         }
6249 };
6250
6251 /**
6252  * Bind key down listener.
6253  *
6254  * @protected
6255  */
6256 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
6257         this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
6258 };
6259
6260 /**
6261  * Unbind key down listener.
6262  *
6263  * @protected
6264  */
6265 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
6266         this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
6267 };
6268
6269 /**
6270  * Scroll item into view, preventing spurious mouse highlight actions from happening.
6271  *
6272  * @param {OO.ui.OptionWidget} item Item to scroll into view
6273  */
6274 OO.ui.SelectWidget.prototype.scrollItemIntoView = function ( item ) {
6275         var widget = this;
6276         // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6277         // and around 100-150 ms after it is finished.
6278         this.blockMouseOverEvents++;
6279         item.scrollElementIntoView().done( function () {
6280                 setTimeout( function () {
6281                         widget.blockMouseOverEvents--;
6282                 }, 200 );
6283         } );
6284 };
6285
6286 /**
6287  * Clear the key-press buffer
6288  *
6289  * @protected
6290  */
6291 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
6292         if ( this.keyPressBufferTimer ) {
6293                 clearTimeout( this.keyPressBufferTimer );
6294                 this.keyPressBufferTimer = null;
6295         }
6296         this.keyPressBuffer = '';
6297 };
6298
6299 /**
6300  * Handle key press events.
6301  *
6302  * @protected
6303  * @param {KeyboardEvent} e Key press event
6304  */
6305 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
6306         var c, filter, item;
6307
6308         if ( !e.charCode ) {
6309                 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
6310                         this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
6311                         return false;
6312                 }
6313                 return;
6314         }
6315         if ( String.fromCodePoint ) {
6316                 c = String.fromCodePoint( e.charCode );
6317         } else {
6318                 c = String.fromCharCode( e.charCode );
6319         }
6320
6321         if ( this.keyPressBufferTimer ) {
6322                 clearTimeout( this.keyPressBufferTimer );
6323         }
6324         this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
6325
6326         item = this.getHighlightedItem() || this.getSelectedItem();
6327
6328         if ( this.keyPressBuffer === c ) {
6329                 // Common (if weird) special case: typing "xxxx" will cycle through all
6330                 // the items beginning with "x".
6331                 if ( item ) {
6332                         item = this.getRelativeSelectableItem( item, 1 );
6333                 }
6334         } else {
6335                 this.keyPressBuffer += c;
6336         }
6337
6338         filter = this.getItemMatcher( this.keyPressBuffer, false );
6339         if ( !item || !filter( item ) ) {
6340                 item = this.getRelativeSelectableItem( item, 1, filter );
6341         }
6342         if ( item ) {
6343                 if ( this.isVisible() && item.constructor.static.highlightable ) {
6344                         this.highlightItem( item );
6345                 } else {
6346                         this.chooseItem( item );
6347                 }
6348                 this.scrollItemIntoView( item );
6349         }
6350
6351         e.preventDefault();
6352         e.stopPropagation();
6353 };
6354
6355 /**
6356  * Get a matcher for the specific string
6357  *
6358  * @protected
6359  * @param {string} s String to match against items
6360  * @param {boolean} [exact=false] Only accept exact matches
6361  * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6362  */
6363 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
6364         var re;
6365
6366         if ( s.normalize ) {
6367                 s = s.normalize();
6368         }
6369         s = exact ? s.trim() : s.replace( /^\s+/, '' );
6370         re = '^\\s*' + s.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6371         if ( exact ) {
6372                 re += '\\s*$';
6373         }
6374         re = new RegExp( re, 'i' );
6375         return function ( item ) {
6376                 var matchText = item.getMatchText();
6377                 if ( matchText.normalize ) {
6378                         matchText = matchText.normalize();
6379                 }
6380                 return re.test( matchText );
6381         };
6382 };
6383
6384 /**
6385  * Bind key press listener.
6386  *
6387  * @protected
6388  */
6389 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
6390         this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
6391 };
6392
6393 /**
6394  * Unbind key down listener.
6395  *
6396  * If you override this, be sure to call this.clearKeyPressBuffer() from your
6397  * implementation.
6398  *
6399  * @protected
6400  */
6401 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
6402         this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
6403         this.clearKeyPressBuffer();
6404 };
6405
6406 /**
6407  * Visibility change handler
6408  *
6409  * @protected
6410  * @param {boolean} visible
6411  */
6412 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
6413         if ( !visible ) {
6414                 this.clearKeyPressBuffer();
6415         }
6416 };
6417
6418 /**
6419  * Get the closest item to a jQuery.Event.
6420  *
6421  * @private
6422  * @param {jQuery.Event} e
6423  * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6424  */
6425 OO.ui.SelectWidget.prototype.findTargetItem = function ( e ) {
6426         var $option = $( e.target ).closest( '.oo-ui-optionWidget' );
6427         if ( !$option.closest( '.oo-ui-selectWidget' ).is( this.$element ) ) {
6428                 return null;
6429         }
6430         return $option.data( 'oo-ui-optionWidget' ) || null;
6431 };
6432
6433 /**
6434  * Get selected item.
6435  *
6436  * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6437  */
6438 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
6439         var i, len;
6440
6441         for ( i = 0, len = this.items.length; i < len; i++ ) {
6442                 if ( this.items[ i ].isSelected() ) {
6443                         return this.items[ i ];
6444                 }
6445         }
6446         return null;
6447 };
6448
6449 /**
6450  * Get highlighted item.
6451  *
6452  * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6453  */
6454 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
6455         var i, len;
6456
6457         for ( i = 0, len = this.items.length; i < len; i++ ) {
6458                 if ( this.items[ i ].isHighlighted() ) {
6459                         return this.items[ i ];
6460                 }
6461         }
6462         return null;
6463 };
6464
6465 /**
6466  * Toggle pressed state.
6467  *
6468  * Press is a state that occurs when a user mouses down on an item, but
6469  * has not yet let go of the mouse. The item may appear selected, but it will not be selected
6470  * until the user releases the mouse.
6471  *
6472  * @param {boolean} pressed An option is being pressed
6473  */
6474 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
6475         if ( pressed === undefined ) {
6476                 pressed = !this.pressed;
6477         }
6478         if ( pressed !== this.pressed ) {
6479                 this.$element
6480                         .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
6481                         .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
6482                 this.pressed = pressed;
6483         }
6484 };
6485
6486 /**
6487  * Highlight an option. If the `item` param is omitted, no options will be highlighted
6488  * and any existing highlight will be removed. The highlight is mutually exclusive.
6489  *
6490  * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
6491  * @fires highlight
6492  * @chainable
6493  */
6494 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
6495         var i, len, highlighted,
6496                 changed = false;
6497
6498         for ( i = 0, len = this.items.length; i < len; i++ ) {
6499                 highlighted = this.items[ i ] === item;
6500                 if ( this.items[ i ].isHighlighted() !== highlighted ) {
6501                         this.items[ i ].setHighlighted( highlighted );
6502                         changed = true;
6503                 }
6504         }
6505         if ( changed ) {
6506                 if ( item ) {
6507                         this.$focusOwner.attr( 'aria-activedescendant', item.getElementId() );
6508                 } else {
6509                         this.$focusOwner.removeAttr( 'aria-activedescendant' );
6510                 }
6511                 this.emit( 'highlight', item );
6512         }
6513
6514         return this;
6515 };
6516
6517 /**
6518  * Fetch an item by its label.
6519  *
6520  * @param {string} label Label of the item to select.
6521  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6522  * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
6523  */
6524 OO.ui.SelectWidget.prototype.getItemFromLabel = function ( label, prefix ) {
6525         var i, item, found,
6526                 len = this.items.length,
6527                 filter = this.getItemMatcher( label, true );
6528
6529         for ( i = 0; i < len; i++ ) {
6530                 item = this.items[ i ];
6531                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
6532                         return item;
6533                 }
6534         }
6535
6536         if ( prefix ) {
6537                 found = null;
6538                 filter = this.getItemMatcher( label, false );
6539                 for ( i = 0; i < len; i++ ) {
6540                         item = this.items[ i ];
6541                         if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
6542                                 if ( found ) {
6543                                         return null;
6544                                 }
6545                                 found = item;
6546                         }
6547                 }
6548                 if ( found ) {
6549                         return found;
6550                 }
6551         }
6552
6553         return null;
6554 };
6555
6556 /**
6557  * Programmatically select an option by its label. If the item does not exist,
6558  * all options will be deselected.
6559  *
6560  * @param {string} [label] Label of the item to select.
6561  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6562  * @fires select
6563  * @chainable
6564  */
6565 OO.ui.SelectWidget.prototype.selectItemByLabel = function ( label, prefix ) {
6566         var itemFromLabel = this.getItemFromLabel( label, !!prefix );
6567         if ( label === undefined || !itemFromLabel ) {
6568                 return this.selectItem();
6569         }
6570         return this.selectItem( itemFromLabel );
6571 };
6572
6573 /**
6574  * Programmatically select an option by its data. If the `data` parameter is omitted,
6575  * or if the item does not exist, all options will be deselected.
6576  *
6577  * @param {Object|string} [data] Value of the item to select, omit to deselect all
6578  * @fires select
6579  * @chainable
6580  */
6581 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
6582         var itemFromData = this.getItemFromData( data );
6583         if ( data === undefined || !itemFromData ) {
6584                 return this.selectItem();
6585         }
6586         return this.selectItem( itemFromData );
6587 };
6588
6589 /**
6590  * Programmatically select an option by its reference. If the `item` parameter is omitted,
6591  * all options will be deselected.
6592  *
6593  * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6594  * @fires select
6595  * @chainable
6596  */
6597 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
6598         var i, len, selected,
6599                 changed = false;
6600
6601         for ( i = 0, len = this.items.length; i < len; i++ ) {
6602                 selected = this.items[ i ] === item;
6603                 if ( this.items[ i ].isSelected() !== selected ) {
6604                         this.items[ i ].setSelected( selected );
6605                         changed = true;
6606                 }
6607         }
6608         if ( changed ) {
6609                 if ( item && !item.constructor.static.highlightable ) {
6610                         if ( item ) {
6611                                 this.$focusOwner.attr( 'aria-activedescendant', item.getElementId() );
6612                         } else {
6613                                 this.$focusOwner.removeAttr( 'aria-activedescendant' );
6614                         }
6615                 }
6616                 this.emit( 'select', item );
6617         }
6618
6619         return this;
6620 };
6621
6622 /**
6623  * Press an item.
6624  *
6625  * Press is a state that occurs when a user mouses down on an item, but has not
6626  * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
6627  * releases the mouse.
6628  *
6629  * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
6630  * @fires press
6631  * @chainable
6632  */
6633 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
6634         var i, len, pressed,
6635                 changed = false;
6636
6637         for ( i = 0, len = this.items.length; i < len; i++ ) {
6638                 pressed = this.items[ i ] === item;
6639                 if ( this.items[ i ].isPressed() !== pressed ) {
6640                         this.items[ i ].setPressed( pressed );
6641                         changed = true;
6642                 }
6643         }
6644         if ( changed ) {
6645                 this.emit( 'press', item );
6646         }
6647
6648         return this;
6649 };
6650
6651 /**
6652  * Choose an item.
6653  *
6654  * Note that ‘choose’ should never be modified programmatically. A user can choose
6655  * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
6656  * use the #selectItem method.
6657  *
6658  * This method is identical to #selectItem, but may vary in subclasses that take additional action
6659  * when users choose an item with the keyboard or mouse.
6660  *
6661  * @param {OO.ui.OptionWidget} item Item to choose
6662  * @fires choose
6663  * @chainable
6664  */
6665 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
6666         if ( item ) {
6667                 this.selectItem( item );
6668                 this.emit( 'choose', item );
6669         }
6670
6671         return this;
6672 };
6673
6674 /**
6675  * Get an option by its position relative to the specified item (or to the start of the option array,
6676  * if item is `null`). The direction in which to search through the option array is specified with a
6677  * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6678  * `null` if there are no options in the array.
6679  *
6680  * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6681  * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6682  * @param {Function} [filter] Only consider items for which this function returns
6683  *  true. Function takes an OO.ui.OptionWidget and returns a boolean.
6684  * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6685  */
6686 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
6687         var currentIndex, nextIndex, i,
6688                 increase = direction > 0 ? 1 : -1,
6689                 len = this.items.length;
6690
6691         if ( item instanceof OO.ui.OptionWidget ) {
6692                 currentIndex = this.items.indexOf( item );
6693                 nextIndex = ( currentIndex + increase + len ) % len;
6694         } else {
6695                 // If no item is selected and moving forward, start at the beginning.
6696                 // If moving backward, start at the end.
6697                 nextIndex = direction > 0 ? 0 : len - 1;
6698         }
6699
6700         for ( i = 0; i < len; i++ ) {
6701                 item = this.items[ nextIndex ];
6702                 if (
6703                         item instanceof OO.ui.OptionWidget && item.isSelectable() &&
6704                         ( !filter || filter( item ) )
6705                 ) {
6706                         return item;
6707                 }
6708                 nextIndex = ( nextIndex + increase + len ) % len;
6709         }
6710         return null;
6711 };
6712
6713 /**
6714  * Get the next selectable item or `null` if there are no selectable items.
6715  * Disabled options and menu-section markers and breaks are not selectable.
6716  *
6717  * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
6718  */
6719 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
6720         return this.getRelativeSelectableItem( null, 1 );
6721 };
6722
6723 /**
6724  * Add an array of options to the select. Optionally, an index number can be used to
6725  * specify an insertion point.
6726  *
6727  * @param {OO.ui.OptionWidget[]} items Items to add
6728  * @param {number} [index] Index to insert items after
6729  * @fires add
6730  * @chainable
6731  */
6732 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
6733         // Mixin method
6734         OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
6735
6736         // Always provide an index, even if it was omitted
6737         this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
6738
6739         return this;
6740 };
6741
6742 /**
6743  * Remove the specified array of options from the select. Options will be detached
6744  * from the DOM, not removed, so they can be reused later. To remove all options from
6745  * the select, you may wish to use the #clearItems method instead.
6746  *
6747  * @param {OO.ui.OptionWidget[]} items Items to remove
6748  * @fires remove
6749  * @chainable
6750  */
6751 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
6752         var i, len, item;
6753
6754         // Deselect items being removed
6755         for ( i = 0, len = items.length; i < len; i++ ) {
6756                 item = items[ i ];
6757                 if ( item.isSelected() ) {
6758                         this.selectItem( null );
6759                 }
6760         }
6761
6762         // Mixin method
6763         OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
6764
6765         this.emit( 'remove', items );
6766
6767         return this;
6768 };
6769
6770 /**
6771  * Clear all options from the select. Options will be detached from the DOM, not removed,
6772  * so that they can be reused later. To remove a subset of options from the select, use
6773  * the #removeItems method.
6774  *
6775  * @fires remove
6776  * @chainable
6777  */
6778 OO.ui.SelectWidget.prototype.clearItems = function () {
6779         var items = this.items.slice();
6780
6781         // Mixin method
6782         OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
6783
6784         // Clear selection
6785         this.selectItem( null );
6786
6787         this.emit( 'remove', items );
6788
6789         return this;
6790 };
6791
6792 /**
6793  * Set the DOM element which has focus while the user is interacting with this SelectWidget.
6794  *
6795  * Currently this is just used to set `aria-activedescendant` on it.
6796  *
6797  * @protected
6798  * @param {jQuery} $focusOwner
6799  */
6800 OO.ui.SelectWidget.prototype.setFocusOwner = function ( $focusOwner ) {
6801         this.$focusOwner = $focusOwner;
6802 };
6803
6804 /**
6805  * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
6806  * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
6807  * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
6808  * options. For more information about options and selects, please see the
6809  * [OOjs UI documentation on MediaWiki][1].
6810  *
6811  *     @example
6812  *     // Decorated options in a select widget
6813  *     var select = new OO.ui.SelectWidget( {
6814  *         items: [
6815  *             new OO.ui.DecoratedOptionWidget( {
6816  *                 data: 'a',
6817  *                 label: 'Option with icon',
6818  *                 icon: 'help'
6819  *             } ),
6820  *             new OO.ui.DecoratedOptionWidget( {
6821  *                 data: 'b',
6822  *                 label: 'Option with indicator',
6823  *                 indicator: 'next'
6824  *             } )
6825  *         ]
6826  *     } );
6827  *     $( 'body' ).append( select.$element );
6828  *
6829  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6830  *
6831  * @class
6832  * @extends OO.ui.OptionWidget
6833  * @mixins OO.ui.mixin.IconElement
6834  * @mixins OO.ui.mixin.IndicatorElement
6835  *
6836  * @constructor
6837  * @param {Object} [config] Configuration options
6838  */
6839 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
6840         // Parent constructor
6841         OO.ui.DecoratedOptionWidget.parent.call( this, config );
6842
6843         // Mixin constructors
6844         OO.ui.mixin.IconElement.call( this, config );
6845         OO.ui.mixin.IndicatorElement.call( this, config );
6846
6847         // Initialization
6848         this.$element
6849                 .addClass( 'oo-ui-decoratedOptionWidget' )
6850                 .prepend( this.$icon )
6851                 .append( this.$indicator );
6852 };
6853
6854 /* Setup */
6855
6856 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
6857 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
6858 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
6859
6860 /**
6861  * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
6862  * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
6863  * the [OOjs UI documentation on MediaWiki] [1] for more information.
6864  *
6865  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6866  *
6867  * @class
6868  * @extends OO.ui.DecoratedOptionWidget
6869  *
6870  * @constructor
6871  * @param {Object} [config] Configuration options
6872  */
6873 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
6874         // Parent constructor
6875         OO.ui.MenuOptionWidget.parent.call( this, config );
6876
6877         // Initialization
6878         this.$element.addClass( 'oo-ui-menuOptionWidget' );
6879 };
6880
6881 /* Setup */
6882
6883 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
6884
6885 /* Static Properties */
6886
6887 /**
6888  * @static
6889  * @inheritdoc
6890  */
6891 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
6892
6893 /**
6894  * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
6895  * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
6896  *
6897  *     @example
6898  *     var myDropdown = new OO.ui.DropdownWidget( {
6899  *         menu: {
6900  *             items: [
6901  *                 new OO.ui.MenuSectionOptionWidget( {
6902  *                     label: 'Dogs'
6903  *                 } ),
6904  *                 new OO.ui.MenuOptionWidget( {
6905  *                     data: 'corgi',
6906  *                     label: 'Welsh Corgi'
6907  *                 } ),
6908  *                 new OO.ui.MenuOptionWidget( {
6909  *                     data: 'poodle',
6910  *                     label: 'Standard Poodle'
6911  *                 } ),
6912  *                 new OO.ui.MenuSectionOptionWidget( {
6913  *                     label: 'Cats'
6914  *                 } ),
6915  *                 new OO.ui.MenuOptionWidget( {
6916  *                     data: 'lion',
6917  *                     label: 'Lion'
6918  *                 } )
6919  *             ]
6920  *         }
6921  *     } );
6922  *     $( 'body' ).append( myDropdown.$element );
6923  *
6924  * @class
6925  * @extends OO.ui.DecoratedOptionWidget
6926  *
6927  * @constructor
6928  * @param {Object} [config] Configuration options
6929  */
6930 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
6931         // Parent constructor
6932         OO.ui.MenuSectionOptionWidget.parent.call( this, config );
6933
6934         // Initialization
6935         this.$element.addClass( 'oo-ui-menuSectionOptionWidget' )
6936                 .removeAttr( 'role aria-selected' );
6937 };
6938
6939 /* Setup */
6940
6941 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
6942
6943 /* Static Properties */
6944
6945 /**
6946  * @static
6947  * @inheritdoc
6948  */
6949 OO.ui.MenuSectionOptionWidget.static.selectable = false;
6950
6951 /**
6952  * @static
6953  * @inheritdoc
6954  */
6955 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
6956
6957 /**
6958  * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
6959  * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
6960  * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
6961  * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
6962  * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
6963  * and customized to be opened, closed, and displayed as needed.
6964  *
6965  * By default, menus are clipped to the visible viewport and are not visible when a user presses the
6966  * mouse outside the menu.
6967  *
6968  * Menus also have support for keyboard interaction:
6969  *
6970  * - Enter/Return key: choose and select a menu option
6971  * - Up-arrow key: highlight the previous menu option
6972  * - Down-arrow key: highlight the next menu option
6973  * - Esc key: hide the menu
6974  *
6975  * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
6976  *
6977  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6978  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6979  *
6980  * @class
6981  * @extends OO.ui.SelectWidget
6982  * @mixins OO.ui.mixin.ClippableElement
6983  * @mixins OO.ui.mixin.FloatableElement
6984  *
6985  * @constructor
6986  * @param {Object} [config] Configuration options
6987  * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
6988  *  the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
6989  *  and {@link OO.ui.mixin.LookupElement LookupElement}
6990  * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
6991  *  the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
6992  * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
6993  *  anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
6994  *  that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
6995  *  that button, unless the button (or its parent widget) is passed in here.
6996  * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
6997  * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
6998  * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
6999  * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7000  * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7001  * @cfg {number} [width] Width of the menu
7002  */
7003 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
7004         // Configuration initialization
7005         config = config || {};
7006
7007         // Parent constructor
7008         OO.ui.MenuSelectWidget.parent.call( this, config );
7009
7010         // Mixin constructors
7011         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
7012         OO.ui.mixin.FloatableElement.call( this, config );
7013
7014         // Properties
7015         this.autoHide = config.autoHide === undefined || !!config.autoHide;
7016         this.hideOnChoose = config.hideOnChoose === undefined || !!config.hideOnChoose;
7017         this.filterFromInput = !!config.filterFromInput;
7018         this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
7019         this.$widget = config.widget ? config.widget.$element : null;
7020         this.$autoCloseIgnore = config.$autoCloseIgnore || $( [] );
7021         this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
7022         this.onInputEditHandler = OO.ui.debounce( this.updateItemVisibility.bind( this ), 100 );
7023         this.highlightOnFilter = !!config.highlightOnFilter;
7024         this.width = config.width;
7025
7026         // Initialization
7027         this.$element.addClass( 'oo-ui-menuSelectWidget' );
7028         if ( config.widget ) {
7029                 this.setFocusOwner( config.widget.$tabIndexed );
7030         }
7031
7032         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7033         // that reference properties not initialized at that time of parent class construction
7034         // TODO: Find a better way to handle post-constructor setup
7035         this.visible = false;
7036         this.$element.addClass( 'oo-ui-element-hidden' );
7037 };
7038
7039 /* Setup */
7040
7041 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
7042 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
7043 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.FloatableElement );
7044
7045 /* Events */
7046
7047 /**
7048  * @event ready
7049  *
7050  * The menu is ready: it is visible and has been positioned and clipped.
7051  */
7052
7053 /* Methods */
7054
7055 /**
7056  * Handles document mouse down events.
7057  *
7058  * @protected
7059  * @param {MouseEvent} e Mouse down event
7060  */
7061 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
7062         if (
7063                 this.isVisible() &&
7064                 !OO.ui.contains(
7065                         this.$element.add( this.$widget ).add( this.$autoCloseIgnore ).get(),
7066                         e.target,
7067                         true
7068                 )
7069         ) {
7070                 this.toggle( false );
7071         }
7072 };
7073
7074 /**
7075  * @inheritdoc
7076  */
7077 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
7078         var currentItem = this.getHighlightedItem() || this.getSelectedItem();
7079
7080         if ( !this.isDisabled() && this.isVisible() ) {
7081                 switch ( e.keyCode ) {
7082                         case OO.ui.Keys.LEFT:
7083                         case OO.ui.Keys.RIGHT:
7084                                 // Do nothing if a text field is associated, arrow keys will be handled natively
7085                                 if ( !this.$input ) {
7086                                         OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
7087                                 }
7088                                 break;
7089                         case OO.ui.Keys.ESCAPE:
7090                         case OO.ui.Keys.TAB:
7091                                 if ( currentItem ) {
7092                                         currentItem.setHighlighted( false );
7093                                 }
7094                                 this.toggle( false );
7095                                 // Don't prevent tabbing away, prevent defocusing
7096                                 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
7097                                         e.preventDefault();
7098                                         e.stopPropagation();
7099                                 }
7100                                 break;
7101                         default:
7102                                 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
7103                                 return;
7104                 }
7105         }
7106 };
7107
7108 /**
7109  * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7110  * or after items were added/removed (always).
7111  *
7112  * @protected
7113  */
7114 OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
7115         var i, item, visible, section, sectionEmpty, filter, exactFilter,
7116                 firstItemFound = false,
7117                 anyVisible = false,
7118                 len = this.items.length,
7119                 showAll = !this.isVisible(),
7120                 exactMatch = false;
7121
7122         if ( this.$input && this.filterFromInput ) {
7123                 filter = showAll ? null : this.getItemMatcher( this.$input.val() );
7124                 exactFilter = this.getItemMatcher( this.$input.val(), true );
7125
7126                 // Hide non-matching options, and also hide section headers if all options
7127                 // in their section are hidden.
7128                 for ( i = 0; i < len; i++ ) {
7129                         item = this.items[ i ];
7130                         if ( item instanceof OO.ui.MenuSectionOptionWidget ) {
7131                                 if ( section ) {
7132                                         // If the previous section was empty, hide its header
7133                                         section.toggle( showAll || !sectionEmpty );
7134                                 }
7135                                 section = item;
7136                                 sectionEmpty = true;
7137                         } else if ( item instanceof OO.ui.OptionWidget ) {
7138                                 visible = showAll || filter( item );
7139                                 exactMatch = exactMatch || exactFilter( item );
7140                                 anyVisible = anyVisible || visible;
7141                                 sectionEmpty = sectionEmpty && !visible;
7142                                 item.toggle( visible );
7143                                 if ( this.highlightOnFilter && visible && !firstItemFound ) {
7144                                         // Highlight the first item in the list
7145                                         this.highlightItem( item );
7146                                         firstItemFound = true;
7147                                 }
7148                         }
7149                 }
7150                 // Process the final section
7151                 if ( section ) {
7152                         section.toggle( showAll || !sectionEmpty );
7153                 }
7154
7155                 if ( anyVisible && this.items.length && !exactMatch ) {
7156                         this.scrollItemIntoView( this.items[ 0 ] );
7157                 }
7158
7159                 this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
7160         }
7161
7162         // Reevaluate clipping
7163         this.clip();
7164 };
7165
7166 /**
7167  * @inheritdoc
7168  */
7169 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
7170         if ( this.$input ) {
7171                 this.$input.on( 'keydown', this.onKeyDownHandler );
7172         } else {
7173                 OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
7174         }
7175 };
7176
7177 /**
7178  * @inheritdoc
7179  */
7180 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
7181         if ( this.$input ) {
7182                 this.$input.off( 'keydown', this.onKeyDownHandler );
7183         } else {
7184                 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
7185         }
7186 };
7187
7188 /**
7189  * @inheritdoc
7190  */
7191 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
7192         if ( this.$input ) {
7193                 if ( this.filterFromInput ) {
7194                         this.$input.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
7195                         this.updateItemVisibility();
7196                 }
7197         } else {
7198                 OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
7199         }
7200 };
7201
7202 /**
7203  * @inheritdoc
7204  */
7205 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
7206         if ( this.$input ) {
7207                 if ( this.filterFromInput ) {
7208                         this.$input.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
7209                         this.updateItemVisibility();
7210                 }
7211         } else {
7212                 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
7213         }
7214 };
7215
7216 /**
7217  * Choose an item.
7218  *
7219  * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
7220  *
7221  * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
7222  * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
7223  *
7224  * @param {OO.ui.OptionWidget} item Item to choose
7225  * @chainable
7226  */
7227 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
7228         OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
7229         if ( this.hideOnChoose ) {
7230                 this.toggle( false );
7231         }
7232         return this;
7233 };
7234
7235 /**
7236  * @inheritdoc
7237  */
7238 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
7239         // Parent method
7240         OO.ui.MenuSelectWidget.parent.prototype.addItems.call( this, items, index );
7241
7242         this.updateItemVisibility();
7243
7244         return this;
7245 };
7246
7247 /**
7248  * @inheritdoc
7249  */
7250 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
7251         // Parent method
7252         OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
7253
7254         this.updateItemVisibility();
7255
7256         return this;
7257 };
7258
7259 /**
7260  * @inheritdoc
7261  */
7262 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
7263         // Parent method
7264         OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
7265
7266         this.updateItemVisibility();
7267
7268         return this;
7269 };
7270
7271 /**
7272  * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7273  * `.toggle( true )` after its #$element is attached to the DOM.
7274  *
7275  * Do not show the menu while it is not attached to the DOM. The calculations required to display
7276  * it in the right place and with the right dimensions only work correctly while it is attached.
7277  * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7278  * strictly enforced, so currently it only generates a warning in the browser console.
7279  *
7280  * @fires ready
7281  * @inheritdoc
7282  */
7283 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
7284         var change;
7285
7286         visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
7287         change = visible !== this.isVisible();
7288
7289         if ( visible && !this.warnedUnattached && !this.isElementAttached() ) {
7290                 OO.ui.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7291                 this.warnedUnattached = true;
7292         }
7293
7294         if ( change && visible && ( this.width || this.$floatableContainer ) ) {
7295                 this.setIdealSize( this.width || this.$floatableContainer.width() );
7296         }
7297
7298         // Parent method
7299         OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
7300
7301         if ( change ) {
7302                 if ( visible ) {
7303                         this.bindKeyDownListener();
7304                         this.bindKeyPressListener();
7305
7306                         this.togglePositioning( !!this.$floatableContainer );
7307                         this.toggleClipping( true );
7308
7309                         this.$focusOwner.attr( 'aria-expanded', 'true' );
7310
7311                         if ( this.getSelectedItem() ) {
7312                                 this.$focusOwner.attr( 'aria-activedescendant', this.getSelectedItem().getElementId() );
7313                                 this.getSelectedItem().scrollElementIntoView( { duration: 0 } );
7314                         }
7315
7316                         // Auto-hide
7317                         if ( this.autoHide ) {
7318                                 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
7319                         }
7320
7321                         this.emit( 'ready' );
7322                 } else {
7323                         this.$focusOwner.removeAttr( 'aria-activedescendant' );
7324                         this.unbindKeyDownListener();
7325                         this.unbindKeyPressListener();
7326                         this.$focusOwner.attr( 'aria-expanded', 'false' );
7327                         this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
7328                         this.togglePositioning( false );
7329                         this.toggleClipping( false );
7330                 }
7331         }
7332
7333         return this;
7334 };
7335
7336 /**
7337  * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7338  * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7339  * users can interact with it.
7340  *
7341  * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7342  * OO.ui.DropdownInputWidget instead.
7343  *
7344  *     @example
7345  *     // Example: A DropdownWidget with a menu that contains three options
7346  *     var dropDown = new OO.ui.DropdownWidget( {
7347  *         label: 'Dropdown menu: Select a menu option',
7348  *         menu: {
7349  *             items: [
7350  *                 new OO.ui.MenuOptionWidget( {
7351  *                     data: 'a',
7352  *                     label: 'First'
7353  *                 } ),
7354  *                 new OO.ui.MenuOptionWidget( {
7355  *                     data: 'b',
7356  *                     label: 'Second'
7357  *                 } ),
7358  *                 new OO.ui.MenuOptionWidget( {
7359  *                     data: 'c',
7360  *                     label: 'Third'
7361  *                 } )
7362  *             ]
7363  *         }
7364  *     } );
7365  *
7366  *     $( 'body' ).append( dropDown.$element );
7367  *
7368  *     dropDown.getMenu().selectItemByData( 'b' );
7369  *
7370  *     dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
7371  *
7372  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
7373  *
7374  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7375  *
7376  * @class
7377  * @extends OO.ui.Widget
7378  * @mixins OO.ui.mixin.IconElement
7379  * @mixins OO.ui.mixin.IndicatorElement
7380  * @mixins OO.ui.mixin.LabelElement
7381  * @mixins OO.ui.mixin.TitledElement
7382  * @mixins OO.ui.mixin.TabIndexedElement
7383  *
7384  * @constructor
7385  * @param {Object} [config] Configuration options
7386  * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.MenuSelectWidget menu select widget}
7387  * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
7388  *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
7389  *  containing `<div>` and has a larger area. By default, the menu uses relative positioning.
7390  *  See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
7391  */
7392 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
7393         // Configuration initialization
7394         config = $.extend( { indicator: 'down' }, config );
7395
7396         // Parent constructor
7397         OO.ui.DropdownWidget.parent.call( this, config );
7398
7399         // Properties (must be set before TabIndexedElement constructor call)
7400         this.$handle = this.$( '<span>' );
7401         this.$overlay = config.$overlay || this.$element;
7402
7403         // Mixin constructors
7404         OO.ui.mixin.IconElement.call( this, config );
7405         OO.ui.mixin.IndicatorElement.call( this, config );
7406         OO.ui.mixin.LabelElement.call( this, config );
7407         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
7408         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
7409
7410         // Properties
7411         this.menu = new OO.ui.MenuSelectWidget( $.extend( {
7412                 widget: this,
7413                 $floatableContainer: this.$element
7414         }, config.menu ) );
7415
7416         // Events
7417         this.$handle.on( {
7418                 click: this.onClick.bind( this ),
7419                 keydown: this.onKeyDown.bind( this ),
7420                 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
7421                 keypress: this.menu.onKeyPressHandler,
7422                 blur: this.menu.clearKeyPressBuffer.bind( this.menu )
7423         } );
7424         this.menu.connect( this, {
7425                 select: 'onMenuSelect',
7426                 toggle: 'onMenuToggle'
7427         } );
7428
7429         // Initialization
7430         this.$handle
7431                 .addClass( 'oo-ui-dropdownWidget-handle' )
7432                 .attr( {
7433                         role: 'combobox',
7434                         'aria-owns': this.menu.getElementId(),
7435                         'aria-autocomplete': 'list'
7436                 } )
7437                 .append( this.$icon, this.$label, this.$indicator );
7438         this.$element
7439                 .addClass( 'oo-ui-dropdownWidget' )
7440                 .append( this.$handle );
7441         this.$overlay.append( this.menu.$element );
7442 };
7443
7444 /* Setup */
7445
7446 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
7447 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
7448 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
7449 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
7450 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
7451 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
7452
7453 /* Methods */
7454
7455 /**
7456  * Get the menu.
7457  *
7458  * @return {OO.ui.MenuSelectWidget} Menu of widget
7459  */
7460 OO.ui.DropdownWidget.prototype.getMenu = function () {
7461         return this.menu;
7462 };
7463
7464 /**
7465  * Handles menu select events.
7466  *
7467  * @private
7468  * @param {OO.ui.MenuOptionWidget} item Selected menu item
7469  */
7470 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
7471         var selectedLabel;
7472
7473         if ( !item ) {
7474                 this.setLabel( null );
7475                 return;
7476         }
7477
7478         selectedLabel = item.getLabel();
7479
7480         // If the label is a DOM element, clone it, because setLabel will append() it
7481         if ( selectedLabel instanceof jQuery ) {
7482                 selectedLabel = selectedLabel.clone();
7483         }
7484
7485         this.setLabel( selectedLabel );
7486 };
7487
7488 /**
7489  * Handle menu toggle events.
7490  *
7491  * @private
7492  * @param {boolean} isVisible Menu toggle event
7493  */
7494 OO.ui.DropdownWidget.prototype.onMenuToggle = function ( isVisible ) {
7495         this.$element.toggleClass( 'oo-ui-dropdownWidget-open', isVisible );
7496         this.$handle.attr(
7497                 'aria-expanded',
7498                 this.$element.hasClass( 'oo-ui-dropdownWidget-open' ).toString()
7499         );
7500 };
7501
7502 /**
7503  * Handle mouse click events.
7504  *
7505  * @private
7506  * @param {jQuery.Event} e Mouse click event
7507  */
7508 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
7509         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
7510                 this.menu.toggle();
7511         }
7512         return false;
7513 };
7514
7515 /**
7516  * Handle key down events.
7517  *
7518  * @private
7519  * @param {jQuery.Event} e Key down event
7520  */
7521 OO.ui.DropdownWidget.prototype.onKeyDown = function ( e ) {
7522         if (
7523                 !this.isDisabled() &&
7524                 (
7525                         e.which === OO.ui.Keys.ENTER ||
7526                         (
7527                                 !this.menu.isVisible() &&
7528                                 (
7529                                         e.which === OO.ui.Keys.SPACE ||
7530                                         e.which === OO.ui.Keys.UP ||
7531                                         e.which === OO.ui.Keys.DOWN
7532                                 )
7533                         )
7534                 )
7535         ) {
7536                 this.menu.toggle();
7537                 return false;
7538         }
7539 };
7540
7541 /**
7542  * RadioOptionWidget is an option widget that looks like a radio button.
7543  * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
7544  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7545  *
7546  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7547  *
7548  * @class
7549  * @extends OO.ui.OptionWidget
7550  *
7551  * @constructor
7552  * @param {Object} [config] Configuration options
7553  */
7554 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
7555         // Configuration initialization
7556         config = config || {};
7557
7558         // Properties (must be done before parent constructor which calls #setDisabled)
7559         this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
7560
7561         // Parent constructor
7562         OO.ui.RadioOptionWidget.parent.call( this, config );
7563
7564         // Initialization
7565         // Remove implicit role, we're handling it ourselves
7566         this.radio.$input.attr( 'role', 'presentation' );
7567         this.$element
7568                 .addClass( 'oo-ui-radioOptionWidget' )
7569                 .attr( 'role', 'radio' )
7570                 .attr( 'aria-checked', 'false' )
7571                 .removeAttr( 'aria-selected' )
7572                 .prepend( this.radio.$element );
7573 };
7574
7575 /* Setup */
7576
7577 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
7578
7579 /* Static Properties */
7580
7581 /**
7582  * @static
7583  * @inheritdoc
7584  */
7585 OO.ui.RadioOptionWidget.static.highlightable = false;
7586
7587 /**
7588  * @static
7589  * @inheritdoc
7590  */
7591 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
7592
7593 /**
7594  * @static
7595  * @inheritdoc
7596  */
7597 OO.ui.RadioOptionWidget.static.pressable = false;
7598
7599 /**
7600  * @static
7601  * @inheritdoc
7602  */
7603 OO.ui.RadioOptionWidget.static.tagName = 'label';
7604
7605 /* Methods */
7606
7607 /**
7608  * @inheritdoc
7609  */
7610 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
7611         OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
7612
7613         this.radio.setSelected( state );
7614         this.$element
7615                 .attr( 'aria-checked', state.toString() )
7616                 .removeAttr( 'aria-selected' );
7617
7618         return this;
7619 };
7620
7621 /**
7622  * @inheritdoc
7623  */
7624 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
7625         OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
7626
7627         this.radio.setDisabled( this.isDisabled() );
7628
7629         return this;
7630 };
7631
7632 /**
7633  * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
7634  * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
7635  * an interface for adding, removing and selecting options.
7636  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
7637  *
7638  * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7639  * OO.ui.RadioSelectInputWidget instead.
7640  *
7641  *     @example
7642  *     // A RadioSelectWidget with RadioOptions.
7643  *     var option1 = new OO.ui.RadioOptionWidget( {
7644  *         data: 'a',
7645  *         label: 'Selected radio option'
7646  *     } );
7647  *
7648  *     var option2 = new OO.ui.RadioOptionWidget( {
7649  *         data: 'b',
7650  *         label: 'Unselected radio option'
7651  *     } );
7652  *
7653  *     var radioSelect=new OO.ui.RadioSelectWidget( {
7654  *         items: [ option1, option2 ]
7655  *      } );
7656  *
7657  *     // Select 'option 1' using the RadioSelectWidget's selectItem() method.
7658  *     radioSelect.selectItem( option1 );
7659  *
7660  *     $( 'body' ).append( radioSelect.$element );
7661  *
7662  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
7663
7664  *
7665  * @class
7666  * @extends OO.ui.SelectWidget
7667  * @mixins OO.ui.mixin.TabIndexedElement
7668  *
7669  * @constructor
7670  * @param {Object} [config] Configuration options
7671  */
7672 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
7673         // Parent constructor
7674         OO.ui.RadioSelectWidget.parent.call( this, config );
7675
7676         // Mixin constructors
7677         OO.ui.mixin.TabIndexedElement.call( this, config );
7678
7679         // Events
7680         this.$element.on( {
7681                 focus: this.bindKeyDownListener.bind( this ),
7682                 blur: this.unbindKeyDownListener.bind( this )
7683         } );
7684
7685         // Initialization
7686         this.$element
7687                 .addClass( 'oo-ui-radioSelectWidget' )
7688                 .attr( 'role', 'radiogroup' );
7689 };
7690
7691 /* Setup */
7692
7693 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
7694 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
7695
7696 /**
7697  * MultioptionWidgets are special elements that can be selected and configured with data. The
7698  * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
7699  * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
7700  * and examples, please see the [OOjs UI documentation on MediaWiki][1].
7701  *
7702  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
7703  *
7704  * @class
7705  * @extends OO.ui.Widget
7706  * @mixins OO.ui.mixin.ItemWidget
7707  * @mixins OO.ui.mixin.LabelElement
7708  *
7709  * @constructor
7710  * @param {Object} [config] Configuration options
7711  * @cfg {boolean} [selected=false] Whether the option is initially selected
7712  */
7713 OO.ui.MultioptionWidget = function OoUiMultioptionWidget( config ) {
7714         // Configuration initialization
7715         config = config || {};
7716
7717         // Parent constructor
7718         OO.ui.MultioptionWidget.parent.call( this, config );
7719
7720         // Mixin constructors
7721         OO.ui.mixin.ItemWidget.call( this );
7722         OO.ui.mixin.LabelElement.call( this, config );
7723
7724         // Properties
7725         this.selected = null;
7726
7727         // Initialization
7728         this.$element
7729                 .addClass( 'oo-ui-multioptionWidget' )
7730                 .append( this.$label );
7731         this.setSelected( config.selected );
7732 };
7733
7734 /* Setup */
7735
7736 OO.inheritClass( OO.ui.MultioptionWidget, OO.ui.Widget );
7737 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.ItemWidget );
7738 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.LabelElement );
7739
7740 /* Events */
7741
7742 /**
7743  * @event change
7744  *
7745  * A change event is emitted when the selected state of the option changes.
7746  *
7747  * @param {boolean} selected Whether the option is now selected
7748  */
7749
7750 /* Methods */
7751
7752 /**
7753  * Check if the option is selected.
7754  *
7755  * @return {boolean} Item is selected
7756  */
7757 OO.ui.MultioptionWidget.prototype.isSelected = function () {
7758         return this.selected;
7759 };
7760
7761 /**
7762  * Set the option’s selected state. In general, all modifications to the selection
7763  * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
7764  * method instead of this method.
7765  *
7766  * @param {boolean} [state=false] Select option
7767  * @chainable
7768  */
7769 OO.ui.MultioptionWidget.prototype.setSelected = function ( state ) {
7770         state = !!state;
7771         if ( this.selected !== state ) {
7772                 this.selected = state;
7773                 this.emit( 'change', state );
7774                 this.$element.toggleClass( 'oo-ui-multioptionWidget-selected', state );
7775         }
7776         return this;
7777 };
7778
7779 /**
7780  * MultiselectWidget allows selecting multiple options from a list.
7781  *
7782  * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
7783  *
7784  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
7785  *
7786  * @class
7787  * @abstract
7788  * @extends OO.ui.Widget
7789  * @mixins OO.ui.mixin.GroupWidget
7790  *
7791  * @constructor
7792  * @param {Object} [config] Configuration options
7793  * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
7794  */
7795 OO.ui.MultiselectWidget = function OoUiMultiselectWidget( config ) {
7796         // Parent constructor
7797         OO.ui.MultiselectWidget.parent.call( this, config );
7798
7799         // Configuration initialization
7800         config = config || {};
7801
7802         // Mixin constructors
7803         OO.ui.mixin.GroupWidget.call( this, config );
7804
7805         // Events
7806         this.aggregate( { change: 'select' } );
7807         // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
7808         // by GroupElement only when items are added/removed
7809         this.connect( this, { select: [ 'emit', 'change' ] } );
7810
7811         // Initialization
7812         if ( config.items ) {
7813                 this.addItems( config.items );
7814         }
7815         this.$group.addClass( 'oo-ui-multiselectWidget-group' );
7816         this.$element.addClass( 'oo-ui-multiselectWidget' )
7817                 .append( this.$group );
7818 };
7819
7820 /* Setup */
7821
7822 OO.inheritClass( OO.ui.MultiselectWidget, OO.ui.Widget );
7823 OO.mixinClass( OO.ui.MultiselectWidget, OO.ui.mixin.GroupWidget );
7824
7825 /* Events */
7826
7827 /**
7828  * @event change
7829  *
7830  * A change event is emitted when the set of items changes, or an item is selected or deselected.
7831  */
7832
7833 /**
7834  * @event select
7835  *
7836  * A select event is emitted when an item is selected or deselected.
7837  */
7838
7839 /* Methods */
7840
7841 /**
7842  * Get options that are selected.
7843  *
7844  * @return {OO.ui.MultioptionWidget[]} Selected options
7845  */
7846 OO.ui.MultiselectWidget.prototype.getSelectedItems = function () {
7847         return this.items.filter( function ( item ) {
7848                 return item.isSelected();
7849         } );
7850 };
7851
7852 /**
7853  * Get the data of options that are selected.
7854  *
7855  * @return {Object[]|string[]} Values of selected options
7856  */
7857 OO.ui.MultiselectWidget.prototype.getSelectedItemsData = function () {
7858         return this.getSelectedItems().map( function ( item ) {
7859                 return item.data;
7860         } );
7861 };
7862
7863 /**
7864  * Select options by reference. Options not mentioned in the `items` array will be deselected.
7865  *
7866  * @param {OO.ui.MultioptionWidget[]} items Items to select
7867  * @chainable
7868  */
7869 OO.ui.MultiselectWidget.prototype.selectItems = function ( items ) {
7870         this.items.forEach( function ( item ) {
7871                 var selected = items.indexOf( item ) !== -1;
7872                 item.setSelected( selected );
7873         } );
7874         return this;
7875 };
7876
7877 /**
7878  * Select items by their data. Options not mentioned in the `datas` array will be deselected.
7879  *
7880  * @param {Object[]|string[]} datas Values of items to select
7881  * @chainable
7882  */
7883 OO.ui.MultiselectWidget.prototype.selectItemsByData = function ( datas ) {
7884         var items,
7885                 widget = this;
7886         items = datas.map( function ( data ) {
7887                 return widget.getItemFromData( data );
7888         } );
7889         this.selectItems( items );
7890         return this;
7891 };
7892
7893 /**
7894  * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
7895  * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
7896  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
7897  *
7898  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
7899  *
7900  * @class
7901  * @extends OO.ui.MultioptionWidget
7902  *
7903  * @constructor
7904  * @param {Object} [config] Configuration options
7905  */
7906 OO.ui.CheckboxMultioptionWidget = function OoUiCheckboxMultioptionWidget( config ) {
7907         // Configuration initialization
7908         config = config || {};
7909
7910         // Properties (must be done before parent constructor which calls #setDisabled)
7911         this.checkbox = new OO.ui.CheckboxInputWidget();
7912
7913         // Parent constructor
7914         OO.ui.CheckboxMultioptionWidget.parent.call( this, config );
7915
7916         // Events
7917         this.checkbox.on( 'change', this.onCheckboxChange.bind( this ) );
7918         this.$element.on( 'keydown', this.onKeyDown.bind( this ) );
7919
7920         // Initialization
7921         this.$element
7922                 .addClass( 'oo-ui-checkboxMultioptionWidget' )
7923                 .prepend( this.checkbox.$element );
7924 };
7925
7926 /* Setup */
7927
7928 OO.inheritClass( OO.ui.CheckboxMultioptionWidget, OO.ui.MultioptionWidget );
7929
7930 /* Static Properties */
7931
7932 /**
7933  * @static
7934  * @inheritdoc
7935  */
7936 OO.ui.CheckboxMultioptionWidget.static.tagName = 'label';
7937
7938 /* Methods */
7939
7940 /**
7941  * Handle checkbox selected state change.
7942  *
7943  * @private
7944  */
7945 OO.ui.CheckboxMultioptionWidget.prototype.onCheckboxChange = function () {
7946         this.setSelected( this.checkbox.isSelected() );
7947 };
7948
7949 /**
7950  * @inheritdoc
7951  */
7952 OO.ui.CheckboxMultioptionWidget.prototype.setSelected = function ( state ) {
7953         OO.ui.CheckboxMultioptionWidget.parent.prototype.setSelected.call( this, state );
7954         this.checkbox.setSelected( state );
7955         return this;
7956 };
7957
7958 /**
7959  * @inheritdoc
7960  */
7961 OO.ui.CheckboxMultioptionWidget.prototype.setDisabled = function ( disabled ) {
7962         OO.ui.CheckboxMultioptionWidget.parent.prototype.setDisabled.call( this, disabled );
7963         this.checkbox.setDisabled( this.isDisabled() );
7964         return this;
7965 };
7966
7967 /**
7968  * Focus the widget.
7969  */
7970 OO.ui.CheckboxMultioptionWidget.prototype.focus = function () {
7971         this.checkbox.focus();
7972 };
7973
7974 /**
7975  * Handle key down events.
7976  *
7977  * @protected
7978  * @param {jQuery.Event} e
7979  */
7980 OO.ui.CheckboxMultioptionWidget.prototype.onKeyDown = function ( e ) {
7981         var
7982                 element = this.getElementGroup(),
7983                 nextItem;
7984
7985         if ( e.keyCode === OO.ui.Keys.LEFT || e.keyCode === OO.ui.Keys.UP ) {
7986                 nextItem = element.getRelativeFocusableItem( this, -1 );
7987         } else if ( e.keyCode === OO.ui.Keys.RIGHT || e.keyCode === OO.ui.Keys.DOWN ) {
7988                 nextItem = element.getRelativeFocusableItem( this, 1 );
7989         }
7990
7991         if ( nextItem ) {
7992                 e.preventDefault();
7993                 nextItem.focus();
7994         }
7995 };
7996
7997 /**
7998  * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
7999  * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8000  * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8001  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
8002  *
8003  * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8004  * OO.ui.CheckboxMultiselectInputWidget instead.
8005  *
8006  *     @example
8007  *     // A CheckboxMultiselectWidget with CheckboxMultioptions.
8008  *     var option1 = new OO.ui.CheckboxMultioptionWidget( {
8009  *         data: 'a',
8010  *         selected: true,
8011  *         label: 'Selected checkbox'
8012  *     } );
8013  *
8014  *     var option2 = new OO.ui.CheckboxMultioptionWidget( {
8015  *         data: 'b',
8016  *         label: 'Unselected checkbox'
8017  *     } );
8018  *
8019  *     var multiselect=new OO.ui.CheckboxMultiselectWidget( {
8020  *         items: [ option1, option2 ]
8021  *      } );
8022  *
8023  *     $( 'body' ).append( multiselect.$element );
8024  *
8025  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
8026  *
8027  * @class
8028  * @extends OO.ui.MultiselectWidget
8029  *
8030  * @constructor
8031  * @param {Object} [config] Configuration options
8032  */
8033 OO.ui.CheckboxMultiselectWidget = function OoUiCheckboxMultiselectWidget( config ) {
8034         // Parent constructor
8035         OO.ui.CheckboxMultiselectWidget.parent.call( this, config );
8036
8037         // Properties
8038         this.$lastClicked = null;
8039
8040         // Events
8041         this.$group.on( 'click', this.onClick.bind( this ) );
8042
8043         // Initialization
8044         this.$element
8045                 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8046 };
8047
8048 /* Setup */
8049
8050 OO.inheritClass( OO.ui.CheckboxMultiselectWidget, OO.ui.MultiselectWidget );
8051
8052 /* Methods */
8053
8054 /**
8055  * Get an option by its position relative to the specified item (or to the start of the option array,
8056  * if item is `null`). The direction in which to search through the option array is specified with a
8057  * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8058  * `null` if there are no options in the array.
8059  *
8060  * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8061  * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8062  * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8063  */
8064 OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function ( item, direction ) {
8065         var currentIndex, nextIndex, i,
8066                 increase = direction > 0 ? 1 : -1,
8067                 len = this.items.length;
8068
8069         if ( item ) {
8070                 currentIndex = this.items.indexOf( item );
8071                 nextIndex = ( currentIndex + increase + len ) % len;
8072         } else {
8073                 // If no item is selected and moving forward, start at the beginning.
8074                 // If moving backward, start at the end.
8075                 nextIndex = direction > 0 ? 0 : len - 1;
8076         }
8077
8078         for ( i = 0; i < len; i++ ) {
8079                 item = this.items[ nextIndex ];
8080                 if ( item && !item.isDisabled() ) {
8081                         return item;
8082                 }
8083                 nextIndex = ( nextIndex + increase + len ) % len;
8084         }
8085         return null;
8086 };
8087
8088 /**
8089  * Handle click events on checkboxes.
8090  *
8091  * @param {jQuery.Event} e
8092  */
8093 OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
8094         var $options, lastClickedIndex, nowClickedIndex, i, direction, wasSelected, items,
8095                 $lastClicked = this.$lastClicked,
8096                 $nowClicked = $( e.target ).closest( '.oo-ui-checkboxMultioptionWidget' )
8097                         .not( '.oo-ui-widget-disabled' );
8098
8099         // Allow selecting multiple options at once by Shift-clicking them
8100         if ( $lastClicked && $nowClicked.length && e.shiftKey ) {
8101                 $options = this.$group.find( '.oo-ui-checkboxMultioptionWidget' );
8102                 lastClickedIndex = $options.index( $lastClicked );
8103                 nowClickedIndex = $options.index( $nowClicked );
8104                 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8105                 // browser. In either case we don't need custom handling.
8106                 if ( nowClickedIndex !== lastClickedIndex ) {
8107                         items = this.items;
8108                         wasSelected = items[ nowClickedIndex ].isSelected();
8109                         direction = nowClickedIndex > lastClickedIndex ? 1 : -1;
8110
8111                         // This depends on the DOM order of the items and the order of the .items array being the same.
8112                         for ( i = lastClickedIndex; i !== nowClickedIndex; i += direction ) {
8113                                 if ( !items[ i ].isDisabled() ) {
8114                                         items[ i ].setSelected( !wasSelected );
8115                                 }
8116                         }
8117                         // For the now-clicked element, use immediate timeout to allow the browser to do its own
8118                         // handling first, then set our value. The order in which events happen is different for
8119                         // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8120                         // non-click actions that change the checkboxes.
8121                         e.preventDefault();
8122                         setTimeout( function () {
8123                                 if ( !items[ nowClickedIndex ].isDisabled() ) {
8124                                         items[ nowClickedIndex ].setSelected( !wasSelected );
8125                                 }
8126                         } );
8127                 }
8128         }
8129
8130         if ( $nowClicked.length ) {
8131                 this.$lastClicked = $nowClicked;
8132         }
8133 };
8134
8135 /**
8136  * Focus the widget
8137  *
8138  * @chainable
8139  */
8140 OO.ui.CheckboxMultiselectWidget.prototype.focus = function () {
8141         var item;
8142         if ( !this.isDisabled() ) {
8143                 item = this.getRelativeFocusableItem( null, 1 );
8144                 if ( item ) {
8145                         item.focus();
8146                 }
8147         }
8148         return this;
8149 };
8150
8151 /**
8152  * @inheritdoc
8153  */
8154 OO.ui.CheckboxMultiselectWidget.prototype.simulateLabelClick = function () {
8155         this.focus();
8156 };
8157
8158 /**
8159  * Progress bars visually display the status of an operation, such as a download,
8160  * and can be either determinate or indeterminate:
8161  *
8162  * - **determinate** process bars show the percent of an operation that is complete.
8163  *
8164  * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8165  *   is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8166  *   not use percentages.
8167  *
8168  * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8169  *
8170  *     @example
8171  *     // Examples of determinate and indeterminate progress bars.
8172  *     var progressBar1 = new OO.ui.ProgressBarWidget( {
8173  *         progress: 33
8174  *     } );
8175  *     var progressBar2 = new OO.ui.ProgressBarWidget();
8176  *
8177  *     // Create a FieldsetLayout to layout progress bars
8178  *     var fieldset = new OO.ui.FieldsetLayout;
8179  *     fieldset.addItems( [
8180  *        new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
8181  *        new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
8182  *     ] );
8183  *     $( 'body' ).append( fieldset.$element );
8184  *
8185  * @class
8186  * @extends OO.ui.Widget
8187  *
8188  * @constructor
8189  * @param {Object} [config] Configuration options
8190  * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8191  *  To create a determinate progress bar, specify a number that reflects the initial percent complete.
8192  *  By default, the progress bar is indeterminate.
8193  */
8194 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
8195         // Configuration initialization
8196         config = config || {};
8197
8198         // Parent constructor
8199         OO.ui.ProgressBarWidget.parent.call( this, config );
8200
8201         // Properties
8202         this.$bar = $( '<div>' );
8203         this.progress = null;
8204
8205         // Initialization
8206         this.setProgress( config.progress !== undefined ? config.progress : false );
8207         this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
8208         this.$element
8209                 .attr( {
8210                         role: 'progressbar',
8211                         'aria-valuemin': 0,
8212                         'aria-valuemax': 100
8213                 } )
8214                 .addClass( 'oo-ui-progressBarWidget' )
8215                 .append( this.$bar );
8216 };
8217
8218 /* Setup */
8219
8220 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
8221
8222 /* Static Properties */
8223
8224 /**
8225  * @static
8226  * @inheritdoc
8227  */
8228 OO.ui.ProgressBarWidget.static.tagName = 'div';
8229
8230 /* Methods */
8231
8232 /**
8233  * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8234  *
8235  * @return {number|boolean} Progress percent
8236  */
8237 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
8238         return this.progress;
8239 };
8240
8241 /**
8242  * Set the percent of the process completed or `false` for an indeterminate process.
8243  *
8244  * @param {number|boolean} progress Progress percent or `false` for indeterminate
8245  */
8246 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
8247         this.progress = progress;
8248
8249         if ( progress !== false ) {
8250                 this.$bar.css( 'width', this.progress + '%' );
8251                 this.$element.attr( 'aria-valuenow', this.progress );
8252         } else {
8253                 this.$bar.css( 'width', '' );
8254                 this.$element.removeAttr( 'aria-valuenow' );
8255         }
8256         this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress === false );
8257 };
8258
8259 /**
8260  * InputWidget is the base class for all input widgets, which
8261  * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8262  * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8263  * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8264  *
8265  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8266  *
8267  * @abstract
8268  * @class
8269  * @extends OO.ui.Widget
8270  * @mixins OO.ui.mixin.FlaggedElement
8271  * @mixins OO.ui.mixin.TabIndexedElement
8272  * @mixins OO.ui.mixin.TitledElement
8273  * @mixins OO.ui.mixin.AccessKeyedElement
8274  *
8275  * @constructor
8276  * @param {Object} [config] Configuration options
8277  * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8278  * @cfg {string} [value=''] The value of the input.
8279  * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8280  * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8281  * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8282  *  before it is accepted.
8283  */
8284 OO.ui.InputWidget = function OoUiInputWidget( config ) {
8285         // Configuration initialization
8286         config = config || {};
8287
8288         // Parent constructor
8289         OO.ui.InputWidget.parent.call( this, config );
8290
8291         // Properties
8292         // See #reusePreInfuseDOM about config.$input
8293         this.$input = config.$input || this.getInputElement( config );
8294         this.value = '';
8295         this.inputFilter = config.inputFilter;
8296
8297         // Mixin constructors
8298         OO.ui.mixin.FlaggedElement.call( this, config );
8299         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
8300         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
8301         OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$input } ) );
8302
8303         // Events
8304         this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
8305
8306         // Initialization
8307         this.$input
8308                 .addClass( 'oo-ui-inputWidget-input' )
8309                 .attr( 'name', config.name )
8310                 .prop( 'disabled', this.isDisabled() );
8311         this.$element
8312                 .addClass( 'oo-ui-inputWidget' )
8313                 .append( this.$input );
8314         this.setValue( config.value );
8315         if ( config.dir ) {
8316                 this.setDir( config.dir );
8317         }
8318         if ( config.inputId !== undefined ) {
8319                 this.setInputId( config.inputId );
8320         }
8321 };
8322
8323 /* Setup */
8324
8325 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
8326 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
8327 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
8328 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
8329 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
8330
8331 /* Static Methods */
8332
8333 /**
8334  * @inheritdoc
8335  */
8336 OO.ui.InputWidget.static.reusePreInfuseDOM = function ( node, config ) {
8337         config = OO.ui.InputWidget.parent.static.reusePreInfuseDOM( node, config );
8338         // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8339         config.$input = $( node ).find( '.oo-ui-inputWidget-input' );
8340         return config;
8341 };
8342
8343 /**
8344  * @inheritdoc
8345  */
8346 OO.ui.InputWidget.static.gatherPreInfuseState = function ( node, config ) {
8347         var state = OO.ui.InputWidget.parent.static.gatherPreInfuseState( node, config );
8348         if ( config.$input && config.$input.length ) {
8349                 state.value = config.$input.val();
8350                 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8351                 state.focus = config.$input.is( ':focus' );
8352         }
8353         return state;
8354 };
8355
8356 /* Events */
8357
8358 /**
8359  * @event change
8360  *
8361  * A change event is emitted when the value of the input changes.
8362  *
8363  * @param {string} value
8364  */
8365
8366 /* Methods */
8367
8368 /**
8369  * Get input element.
8370  *
8371  * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
8372  * different circumstances. The element must have a `value` property (like form elements).
8373  *
8374  * @protected
8375  * @param {Object} config Configuration options
8376  * @return {jQuery} Input element
8377  */
8378 OO.ui.InputWidget.prototype.getInputElement = function () {
8379         return $( '<input>' );
8380 };
8381
8382 /**
8383  * Handle potentially value-changing events.
8384  *
8385  * @private
8386  * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8387  */
8388 OO.ui.InputWidget.prototype.onEdit = function () {
8389         var widget = this;
8390         if ( !this.isDisabled() ) {
8391                 // Allow the stack to clear so the value will be updated
8392                 setTimeout( function () {
8393                         widget.setValue( widget.$input.val() );
8394                 } );
8395         }
8396 };
8397
8398 /**
8399  * Get the value of the input.
8400  *
8401  * @return {string} Input value
8402  */
8403 OO.ui.InputWidget.prototype.getValue = function () {
8404         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8405         // it, and we won't know unless they're kind enough to trigger a 'change' event.
8406         var value = this.$input.val();
8407         if ( this.value !== value ) {
8408                 this.setValue( value );
8409         }
8410         return this.value;
8411 };
8412
8413 /**
8414  * Set the directionality of the input.
8415  *
8416  * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
8417  * @chainable
8418  */
8419 OO.ui.InputWidget.prototype.setDir = function ( dir ) {
8420         this.$input.prop( 'dir', dir );
8421         return this;
8422 };
8423
8424 /**
8425  * Set the value of the input.
8426  *
8427  * @param {string} value New value
8428  * @fires change
8429  * @chainable
8430  */
8431 OO.ui.InputWidget.prototype.setValue = function ( value ) {
8432         value = this.cleanUpValue( value );
8433         // Update the DOM if it has changed. Note that with cleanUpValue, it
8434         // is possible for the DOM value to change without this.value changing.
8435         if ( this.$input.val() !== value ) {
8436                 this.$input.val( value );
8437         }
8438         if ( this.value !== value ) {
8439                 this.value = value;
8440                 this.emit( 'change', this.value );
8441         }
8442         return this;
8443 };
8444
8445 /**
8446  * Clean up incoming value.
8447  *
8448  * Ensures value is a string, and converts undefined and null to empty string.
8449  *
8450  * @private
8451  * @param {string} value Original value
8452  * @return {string} Cleaned up value
8453  */
8454 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
8455         if ( value === undefined || value === null ) {
8456                 return '';
8457         } else if ( this.inputFilter ) {
8458                 return this.inputFilter( String( value ) );
8459         } else {
8460                 return String( value );
8461         }
8462 };
8463
8464 /**
8465  * @inheritdoc
8466  */
8467 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
8468         OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
8469         if ( this.$input ) {
8470                 this.$input.prop( 'disabled', this.isDisabled() );
8471         }
8472         return this;
8473 };
8474
8475 /**
8476  * Set the 'id' attribute of the `<input>` element.
8477  *
8478  * @param {string} id
8479  * @chainable
8480  */
8481 OO.ui.InputWidget.prototype.setInputId = function ( id ) {
8482         this.$input.attr( 'id', id );
8483         return this;
8484 };
8485
8486 /**
8487  * @inheritdoc
8488  */
8489 OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
8490         OO.ui.InputWidget.parent.prototype.restorePreInfuseState.call( this, state );
8491         if ( state.value !== undefined && state.value !== this.getValue() ) {
8492                 this.setValue( state.value );
8493         }
8494         if ( state.focus ) {
8495                 this.focus();
8496         }
8497 };
8498
8499 /**
8500  * Data widget intended for creating 'hidden'-type inputs.
8501  *
8502  * @class
8503  * @extends OO.ui.Widget
8504  *
8505  * @constructor
8506  * @param {Object} [config] Configuration options
8507  * @cfg {string} [value=''] The value of the input.
8508  * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8509  */
8510 OO.ui.HiddenInputWidget = function OoUiHiddenInputWidget( config ) {
8511         // Configuration initialization
8512         config = $.extend( { value: '', name: '' }, config );
8513
8514         // Parent constructor
8515         OO.ui.HiddenInputWidget.parent.call( this, config );
8516
8517         // Initialization
8518         this.$element.attr( {
8519                 type: 'hidden',
8520                 value: config.value,
8521                 name: config.name
8522         } );
8523         this.$element.removeAttr( 'aria-disabled' );
8524 };
8525
8526 /* Setup */
8527
8528 OO.inheritClass( OO.ui.HiddenInputWidget, OO.ui.Widget );
8529
8530 /* Static Properties */
8531
8532 /**
8533  * @static
8534  * @inheritdoc
8535  */
8536 OO.ui.HiddenInputWidget.static.tagName = 'input';
8537
8538 /**
8539  * ButtonInputWidget is used to submit HTML forms and is intended to be used within
8540  * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
8541  * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
8542  * HTML `<button>` (the default) or an HTML `<input>` tags. See the
8543  * [OOjs UI documentation on MediaWiki] [1] for more information.
8544  *
8545  *     @example
8546  *     // A ButtonInputWidget rendered as an HTML button, the default.
8547  *     var button = new OO.ui.ButtonInputWidget( {
8548  *         label: 'Input button',
8549  *         icon: 'check',
8550  *         value: 'check'
8551  *     } );
8552  *     $( 'body' ).append( button.$element );
8553  *
8554  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
8555  *
8556  * @class
8557  * @extends OO.ui.InputWidget
8558  * @mixins OO.ui.mixin.ButtonElement
8559  * @mixins OO.ui.mixin.IconElement
8560  * @mixins OO.ui.mixin.IndicatorElement
8561  * @mixins OO.ui.mixin.LabelElement
8562  * @mixins OO.ui.mixin.TitledElement
8563  *
8564  * @constructor
8565  * @param {Object} [config] Configuration options
8566  * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
8567  * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
8568  *  Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
8569  *  non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
8570  *  be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
8571  */
8572 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
8573         // Configuration initialization
8574         config = $.extend( { type: 'button', useInputTag: false }, config );
8575
8576         // See InputWidget#reusePreInfuseDOM about config.$input
8577         if ( config.$input ) {
8578                 config.$input.empty();
8579         }
8580
8581         // Properties (must be set before parent constructor, which calls #setValue)
8582         this.useInputTag = config.useInputTag;
8583
8584         // Parent constructor
8585         OO.ui.ButtonInputWidget.parent.call( this, config );
8586
8587         // Mixin constructors
8588         OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
8589         OO.ui.mixin.IconElement.call( this, config );
8590         OO.ui.mixin.IndicatorElement.call( this, config );
8591         OO.ui.mixin.LabelElement.call( this, config );
8592         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
8593
8594         // Initialization
8595         if ( !config.useInputTag ) {
8596                 this.$input.append( this.$icon, this.$label, this.$indicator );
8597         }
8598         this.$element.addClass( 'oo-ui-buttonInputWidget' );
8599 };
8600
8601 /* Setup */
8602
8603 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
8604 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
8605 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
8606 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
8607 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
8608 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
8609
8610 /* Static Properties */
8611
8612 /**
8613  * @static
8614  * @inheritdoc
8615  */
8616 OO.ui.ButtonInputWidget.static.tagName = 'span';
8617
8618 /* Methods */
8619
8620 /**
8621  * @inheritdoc
8622  * @protected
8623  */
8624 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
8625         var type;
8626         type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ? config.type : 'button';
8627         return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
8628 };
8629
8630 /**
8631  * Set label value.
8632  *
8633  * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
8634  *
8635  * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
8636  *  text, or `null` for no label
8637  * @chainable
8638  */
8639 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
8640         if ( typeof label === 'function' ) {
8641                 label = OO.ui.resolveMsg( label );
8642         }
8643
8644         if ( this.useInputTag ) {
8645                 // Discard non-plaintext labels
8646                 if ( typeof label !== 'string' ) {
8647                         label = '';
8648                 }
8649
8650                 this.$input.val( label );
8651         }
8652
8653         return OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
8654 };
8655
8656 /**
8657  * Set the value of the input.
8658  *
8659  * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
8660  * they do not support {@link #value values}.
8661  *
8662  * @param {string} value New value
8663  * @chainable
8664  */
8665 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
8666         if ( !this.useInputTag ) {
8667                 OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
8668         }
8669         return this;
8670 };
8671
8672 /**
8673  * @inheritdoc
8674  */
8675 OO.ui.ButtonInputWidget.prototype.getInputId = function () {
8676         // Disable generating `<label>` elements for buttons. One would very rarely need additional label
8677         // for a button, and it's already a big clickable target, and it causes unexpected rendering.
8678         return null;
8679 };
8680
8681 /**
8682  * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
8683  * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
8684  * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
8685  * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
8686  *
8687  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
8688  *
8689  *     @example
8690  *     // An example of selected, unselected, and disabled checkbox inputs
8691  *     var checkbox1=new OO.ui.CheckboxInputWidget( {
8692  *          value: 'a',
8693  *          selected: true
8694  *     } );
8695  *     var checkbox2=new OO.ui.CheckboxInputWidget( {
8696  *         value: 'b'
8697  *     } );
8698  *     var checkbox3=new OO.ui.CheckboxInputWidget( {
8699  *         value:'c',
8700  *         disabled: true
8701  *     } );
8702  *     // Create a fieldset layout with fields for each checkbox.
8703  *     var fieldset = new OO.ui.FieldsetLayout( {
8704  *         label: 'Checkboxes'
8705  *     } );
8706  *     fieldset.addItems( [
8707  *         new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
8708  *         new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
8709  *         new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
8710  *     ] );
8711  *     $( 'body' ).append( fieldset.$element );
8712  *
8713  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8714  *
8715  * @class
8716  * @extends OO.ui.InputWidget
8717  *
8718  * @constructor
8719  * @param {Object} [config] Configuration options
8720  * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
8721  */
8722 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
8723         // Configuration initialization
8724         config = config || {};
8725
8726         // Parent constructor
8727         OO.ui.CheckboxInputWidget.parent.call( this, config );
8728
8729         // Initialization
8730         this.$element
8731                 .addClass( 'oo-ui-checkboxInputWidget' )
8732                 // Required for pretty styling in WikimediaUI theme
8733                 .append( $( '<span>' ) );
8734         this.setSelected( config.selected !== undefined ? config.selected : false );
8735 };
8736
8737 /* Setup */
8738
8739 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
8740
8741 /* Static Properties */
8742
8743 /**
8744  * @static
8745  * @inheritdoc
8746  */
8747 OO.ui.CheckboxInputWidget.static.tagName = 'span';
8748
8749 /* Static Methods */
8750
8751 /**
8752  * @inheritdoc
8753  */
8754 OO.ui.CheckboxInputWidget.static.gatherPreInfuseState = function ( node, config ) {
8755         var state = OO.ui.CheckboxInputWidget.parent.static.gatherPreInfuseState( node, config );
8756         state.checked = config.$input.prop( 'checked' );
8757         return state;
8758 };
8759
8760 /* Methods */
8761
8762 /**
8763  * @inheritdoc
8764  * @protected
8765  */
8766 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
8767         return $( '<input>' ).attr( 'type', 'checkbox' );
8768 };
8769
8770 /**
8771  * @inheritdoc
8772  */
8773 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
8774         var widget = this;
8775         if ( !this.isDisabled() ) {
8776                 // Allow the stack to clear so the value will be updated
8777                 setTimeout( function () {
8778                         widget.setSelected( widget.$input.prop( 'checked' ) );
8779                 } );
8780         }
8781 };
8782
8783 /**
8784  * Set selection state of this checkbox.
8785  *
8786  * @param {boolean} state `true` for selected
8787  * @chainable
8788  */
8789 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
8790         state = !!state;
8791         if ( this.selected !== state ) {
8792                 this.selected = state;
8793                 this.$input.prop( 'checked', this.selected );
8794                 this.emit( 'change', this.selected );
8795         }
8796         return this;
8797 };
8798
8799 /**
8800  * Check if this checkbox is selected.
8801  *
8802  * @return {boolean} Checkbox is selected
8803  */
8804 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
8805         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8806         // it, and we won't know unless they're kind enough to trigger a 'change' event.
8807         var selected = this.$input.prop( 'checked' );
8808         if ( this.selected !== selected ) {
8809                 this.setSelected( selected );
8810         }
8811         return this.selected;
8812 };
8813
8814 /**
8815  * @inheritdoc
8816  */
8817 OO.ui.CheckboxInputWidget.prototype.simulateLabelClick = function () {
8818         if ( !this.isDisabled() ) {
8819                 this.$input.click();
8820         }
8821         this.focus();
8822 };
8823
8824 /**
8825  * @inheritdoc
8826  */
8827 OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
8828         OO.ui.CheckboxInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
8829         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
8830                 this.setSelected( state.checked );
8831         }
8832 };
8833
8834 /**
8835  * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
8836  * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8837  * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8838  * more information about input widgets.
8839  *
8840  * A DropdownInputWidget always has a value (one of the options is always selected), unless there
8841  * are no options. If no `value` configuration option is provided, the first option is selected.
8842  * If you need a state representing no value (no option being selected), use a DropdownWidget.
8843  *
8844  * This and OO.ui.RadioSelectInputWidget support the same configuration options.
8845  *
8846  *     @example
8847  *     // Example: A DropdownInputWidget with three options
8848  *     var dropdownInput = new OO.ui.DropdownInputWidget( {
8849  *         options: [
8850  *             { data: 'a', label: 'First' },
8851  *             { data: 'b', label: 'Second'},
8852  *             { data: 'c', label: 'Third' }
8853  *         ]
8854  *     } );
8855  *     $( 'body' ).append( dropdownInput.$element );
8856  *
8857  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8858  *
8859  * @class
8860  * @extends OO.ui.InputWidget
8861  * @mixins OO.ui.mixin.TitledElement
8862  *
8863  * @constructor
8864  * @param {Object} [config] Configuration options
8865  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8866  * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
8867  */
8868 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
8869         // Configuration initialization
8870         config = config || {};
8871
8872         // See InputWidget#reusePreInfuseDOM about config.$input
8873         if ( config.$input ) {
8874                 config.$input.addClass( 'oo-ui-element-hidden' );
8875         }
8876
8877         // Properties (must be done before parent constructor which calls #setDisabled)
8878         this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
8879
8880         // Parent constructor
8881         OO.ui.DropdownInputWidget.parent.call( this, config );
8882
8883         // Mixin constructors
8884         OO.ui.mixin.TitledElement.call( this, config );
8885
8886         // Events
8887         this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
8888
8889         // Initialization
8890         this.setOptions( config.options || [] );
8891         // Set the value again, after we did setOptions(). The call from parent doesn't work because the
8892         // widget has no valid options when it happens.
8893         this.setValue( config.value );
8894         this.$element
8895                 .addClass( 'oo-ui-dropdownInputWidget' )
8896                 .append( this.dropdownWidget.$element );
8897         this.setTabIndexedElement( null );
8898 };
8899
8900 /* Setup */
8901
8902 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
8903 OO.mixinClass( OO.ui.DropdownInputWidget, OO.ui.mixin.TitledElement );
8904
8905 /* Methods */
8906
8907 /**
8908  * @inheritdoc
8909  * @protected
8910  */
8911 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
8912         return $( '<input>' ).attr( 'type', 'hidden' );
8913 };
8914
8915 /**
8916  * Handles menu select events.
8917  *
8918  * @private
8919  * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
8920  */
8921 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
8922         this.setValue( item ? item.getData() : '' );
8923 };
8924
8925 /**
8926  * @inheritdoc
8927  */
8928 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
8929         var selected;
8930         value = this.cleanUpValue( value );
8931         // Only allow setting values that are actually present in the dropdown
8932         selected = this.dropdownWidget.getMenu().getItemFromData( value ) ||
8933                 this.dropdownWidget.getMenu().getFirstSelectableItem();
8934         this.dropdownWidget.getMenu().selectItem( selected );
8935         value = selected ? selected.getData() : '';
8936         OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
8937         return this;
8938 };
8939
8940 /**
8941  * @inheritdoc
8942  */
8943 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
8944         this.dropdownWidget.setDisabled( state );
8945         OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
8946         return this;
8947 };
8948
8949 /**
8950  * Set the options available for this input.
8951  *
8952  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8953  * @chainable
8954  */
8955 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
8956         var
8957                 value = this.getValue(),
8958                 widget = this;
8959
8960         // Rebuild the dropdown menu
8961         this.dropdownWidget.getMenu()
8962                 .clearItems()
8963                 .addItems( options.map( function ( opt ) {
8964                         var optValue = widget.cleanUpValue( opt.data );
8965
8966                         if ( opt.optgroup === undefined ) {
8967                                 return new OO.ui.MenuOptionWidget( {
8968                                         data: optValue,
8969                                         label: opt.label !== undefined ? opt.label : optValue
8970                                 } );
8971                         } else {
8972                                 return new OO.ui.MenuSectionOptionWidget( {
8973                                         label: opt.optgroup
8974                                 } );
8975                         }
8976                 } ) );
8977
8978         // Restore the previous value, or reset to something sensible
8979         if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
8980                 // Previous value is still available, ensure consistency with the dropdown
8981                 this.setValue( value );
8982         } else {
8983                 // No longer valid, reset
8984                 if ( options.length ) {
8985                         this.setValue( options[ 0 ].data );
8986                 }
8987         }
8988
8989         return this;
8990 };
8991
8992 /**
8993  * @inheritdoc
8994  */
8995 OO.ui.DropdownInputWidget.prototype.focus = function () {
8996         this.dropdownWidget.focus();
8997         return this;
8998 };
8999
9000 /**
9001  * @inheritdoc
9002  */
9003 OO.ui.DropdownInputWidget.prototype.blur = function () {
9004         this.dropdownWidget.blur();
9005         return this;
9006 };
9007
9008 /**
9009  * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9010  * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9011  * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9012  * please see the [OOjs UI documentation on MediaWiki][1].
9013  *
9014  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9015  *
9016  *     @example
9017  *     // An example of selected, unselected, and disabled radio inputs
9018  *     var radio1 = new OO.ui.RadioInputWidget( {
9019  *         value: 'a',
9020  *         selected: true
9021  *     } );
9022  *     var radio2 = new OO.ui.RadioInputWidget( {
9023  *         value: 'b'
9024  *     } );
9025  *     var radio3 = new OO.ui.RadioInputWidget( {
9026  *         value: 'c',
9027  *         disabled: true
9028  *     } );
9029  *     // Create a fieldset layout with fields for each radio button.
9030  *     var fieldset = new OO.ui.FieldsetLayout( {
9031  *         label: 'Radio inputs'
9032  *     } );
9033  *     fieldset.addItems( [
9034  *         new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9035  *         new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9036  *         new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9037  *     ] );
9038  *     $( 'body' ).append( fieldset.$element );
9039  *
9040  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9041  *
9042  * @class
9043  * @extends OO.ui.InputWidget
9044  *
9045  * @constructor
9046  * @param {Object} [config] Configuration options
9047  * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9048  */
9049 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
9050         // Configuration initialization
9051         config = config || {};
9052
9053         // Parent constructor
9054         OO.ui.RadioInputWidget.parent.call( this, config );
9055
9056         // Initialization
9057         this.$element
9058                 .addClass( 'oo-ui-radioInputWidget' )
9059                 // Required for pretty styling in WikimediaUI theme
9060                 .append( $( '<span>' ) );
9061         this.setSelected( config.selected !== undefined ? config.selected : false );
9062 };
9063
9064 /* Setup */
9065
9066 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
9067
9068 /* Static Properties */
9069
9070 /**
9071  * @static
9072  * @inheritdoc
9073  */
9074 OO.ui.RadioInputWidget.static.tagName = 'span';
9075
9076 /* Static Methods */
9077
9078 /**
9079  * @inheritdoc
9080  */
9081 OO.ui.RadioInputWidget.static.gatherPreInfuseState = function ( node, config ) {
9082         var state = OO.ui.RadioInputWidget.parent.static.gatherPreInfuseState( node, config );
9083         state.checked = config.$input.prop( 'checked' );
9084         return state;
9085 };
9086
9087 /* Methods */
9088
9089 /**
9090  * @inheritdoc
9091  * @protected
9092  */
9093 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
9094         return $( '<input>' ).attr( 'type', 'radio' );
9095 };
9096
9097 /**
9098  * @inheritdoc
9099  */
9100 OO.ui.RadioInputWidget.prototype.onEdit = function () {
9101         // RadioInputWidget doesn't track its state.
9102 };
9103
9104 /**
9105  * Set selection state of this radio button.
9106  *
9107  * @param {boolean} state `true` for selected
9108  * @chainable
9109  */
9110 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
9111         // RadioInputWidget doesn't track its state.
9112         this.$input.prop( 'checked', state );
9113         return this;
9114 };
9115
9116 /**
9117  * Check if this radio button is selected.
9118  *
9119  * @return {boolean} Radio is selected
9120  */
9121 OO.ui.RadioInputWidget.prototype.isSelected = function () {
9122         return this.$input.prop( 'checked' );
9123 };
9124
9125 /**
9126  * @inheritdoc
9127  */
9128 OO.ui.RadioInputWidget.prototype.simulateLabelClick = function () {
9129         if ( !this.isDisabled() ) {
9130                 this.$input.click();
9131         }
9132         this.focus();
9133 };
9134
9135 /**
9136  * @inheritdoc
9137  */
9138 OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
9139         OO.ui.RadioInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
9140         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
9141                 this.setSelected( state.checked );
9142         }
9143 };
9144
9145 /**
9146  * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9147  * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9148  * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
9149  * more information about input widgets.
9150  *
9151  * This and OO.ui.DropdownInputWidget support the same configuration options.
9152  *
9153  *     @example
9154  *     // Example: A RadioSelectInputWidget with three options
9155  *     var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9156  *         options: [
9157  *             { data: 'a', label: 'First' },
9158  *             { data: 'b', label: 'Second'},
9159  *             { data: 'c', label: 'Third' }
9160  *         ]
9161  *     } );
9162  *     $( 'body' ).append( radioSelectInput.$element );
9163  *
9164  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9165  *
9166  * @class
9167  * @extends OO.ui.InputWidget
9168  *
9169  * @constructor
9170  * @param {Object} [config] Configuration options
9171  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9172  */
9173 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
9174         // Configuration initialization
9175         config = config || {};
9176
9177         // Properties (must be done before parent constructor which calls #setDisabled)
9178         this.radioSelectWidget = new OO.ui.RadioSelectWidget();
9179
9180         // Parent constructor
9181         OO.ui.RadioSelectInputWidget.parent.call( this, config );
9182
9183         // Events
9184         this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
9185
9186         // Initialization
9187         this.setOptions( config.options || [] );
9188         this.$element
9189                 .addClass( 'oo-ui-radioSelectInputWidget' )
9190                 .append( this.radioSelectWidget.$element );
9191         this.setTabIndexedElement( null );
9192 };
9193
9194 /* Setup */
9195
9196 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
9197
9198 /* Static Methods */
9199
9200 /**
9201  * @inheritdoc
9202  */
9203 OO.ui.RadioSelectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
9204         var state = OO.ui.RadioSelectInputWidget.parent.static.gatherPreInfuseState( node, config );
9205         state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9206         return state;
9207 };
9208
9209 /**
9210  * @inheritdoc
9211  */
9212 OO.ui.RadioSelectInputWidget.static.reusePreInfuseDOM = function ( node, config ) {
9213         config = OO.ui.RadioSelectInputWidget.parent.static.reusePreInfuseDOM( node, config );
9214         // Cannot reuse the `<input type=radio>` set
9215         delete config.$input;
9216         return config;
9217 };
9218
9219 /* Methods */
9220
9221 /**
9222  * @inheritdoc
9223  * @protected
9224  */
9225 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
9226         return $( '<input>' ).attr( 'type', 'hidden' );
9227 };
9228
9229 /**
9230  * Handles menu select events.
9231  *
9232  * @private
9233  * @param {OO.ui.RadioOptionWidget} item Selected menu item
9234  */
9235 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
9236         this.setValue( item.getData() );
9237 };
9238
9239 /**
9240  * @inheritdoc
9241  */
9242 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
9243         value = this.cleanUpValue( value );
9244         this.radioSelectWidget.selectItemByData( value );
9245         OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
9246         return this;
9247 };
9248
9249 /**
9250  * @inheritdoc
9251  */
9252 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
9253         this.radioSelectWidget.setDisabled( state );
9254         OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
9255         return this;
9256 };
9257
9258 /**
9259  * Set the options available for this input.
9260  *
9261  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9262  * @chainable
9263  */
9264 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
9265         var
9266                 value = this.getValue(),
9267                 widget = this;
9268
9269         // Rebuild the radioSelect menu
9270         this.radioSelectWidget
9271                 .clearItems()
9272                 .addItems( options.map( function ( opt ) {
9273                         var optValue = widget.cleanUpValue( opt.data );
9274                         return new OO.ui.RadioOptionWidget( {
9275                                 data: optValue,
9276                                 label: opt.label !== undefined ? opt.label : optValue
9277                         } );
9278                 } ) );
9279
9280         // Restore the previous value, or reset to something sensible
9281         if ( this.radioSelectWidget.getItemFromData( value ) ) {
9282                 // Previous value is still available, ensure consistency with the radioSelect
9283                 this.setValue( value );
9284         } else {
9285                 // No longer valid, reset
9286                 if ( options.length ) {
9287                         this.setValue( options[ 0 ].data );
9288                 }
9289         }
9290
9291         return this;
9292 };
9293
9294 /**
9295  * @inheritdoc
9296  */
9297 OO.ui.RadioSelectInputWidget.prototype.focus = function () {
9298         this.radioSelectWidget.focus();
9299         return this;
9300 };
9301
9302 /**
9303  * @inheritdoc
9304  */
9305 OO.ui.RadioSelectInputWidget.prototype.blur = function () {
9306         this.radioSelectWidget.blur();
9307         return this;
9308 };
9309
9310 /**
9311  * CheckboxMultiselectInputWidget is a
9312  * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
9313  * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
9314  * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
9315  * more information about input widgets.
9316  *
9317  *     @example
9318  *     // Example: A CheckboxMultiselectInputWidget with three options
9319  *     var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
9320  *         options: [
9321  *             { data: 'a', label: 'First' },
9322  *             { data: 'b', label: 'Second'},
9323  *             { data: 'c', label: 'Third' }
9324  *         ]
9325  *     } );
9326  *     $( 'body' ).append( multiselectInput.$element );
9327  *
9328  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9329  *
9330  * @class
9331  * @extends OO.ui.InputWidget
9332  *
9333  * @constructor
9334  * @param {Object} [config] Configuration options
9335  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
9336  */
9337 OO.ui.CheckboxMultiselectInputWidget = function OoUiCheckboxMultiselectInputWidget( config ) {
9338         // Configuration initialization
9339         config = config || {};
9340
9341         // Properties (must be done before parent constructor which calls #setDisabled)
9342         this.checkboxMultiselectWidget = new OO.ui.CheckboxMultiselectWidget();
9343
9344         // Parent constructor
9345         OO.ui.CheckboxMultiselectInputWidget.parent.call( this, config );
9346
9347         // Properties
9348         this.inputName = config.name;
9349
9350         // Initialization
9351         this.$element
9352                 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
9353                 .append( this.checkboxMultiselectWidget.$element );
9354         // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
9355         this.$input.detach();
9356         this.setOptions( config.options || [] );
9357         // Have to repeat this from parent, as we need options to be set up for this to make sense
9358         this.setValue( config.value );
9359
9360         // setValue when checkboxMultiselectWidget changes
9361         this.checkboxMultiselectWidget.on( 'change', function () {
9362                 this.setValue( this.checkboxMultiselectWidget.getSelectedItemsData() );
9363         }.bind( this ) );
9364 };
9365
9366 /* Setup */
9367
9368 OO.inheritClass( OO.ui.CheckboxMultiselectInputWidget, OO.ui.InputWidget );
9369
9370 /* Static Methods */
9371
9372 /**
9373  * @inheritdoc
9374  */
9375 OO.ui.CheckboxMultiselectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
9376         var state = OO.ui.CheckboxMultiselectInputWidget.parent.static.gatherPreInfuseState( node, config );
9377         state.value = $( node ).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9378                 .toArray().map( function ( el ) { return el.value; } );
9379         return state;
9380 };
9381
9382 /**
9383  * @inheritdoc
9384  */
9385 OO.ui.CheckboxMultiselectInputWidget.static.reusePreInfuseDOM = function ( node, config ) {
9386         config = OO.ui.CheckboxMultiselectInputWidget.parent.static.reusePreInfuseDOM( node, config );
9387         // Cannot reuse the `<input type=checkbox>` set
9388         delete config.$input;
9389         return config;
9390 };
9391
9392 /* Methods */
9393
9394 /**
9395  * @inheritdoc
9396  * @protected
9397  */
9398 OO.ui.CheckboxMultiselectInputWidget.prototype.getInputElement = function () {
9399         // Actually unused
9400         return $( '<unused>' );
9401 };
9402
9403 /**
9404  * @inheritdoc
9405  */
9406 OO.ui.CheckboxMultiselectInputWidget.prototype.getValue = function () {
9407         var value = this.$element.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9408                 .toArray().map( function ( el ) { return el.value; } );
9409         if ( this.value !== value ) {
9410                 this.setValue( value );
9411         }
9412         return this.value;
9413 };
9414
9415 /**
9416  * @inheritdoc
9417  */
9418 OO.ui.CheckboxMultiselectInputWidget.prototype.setValue = function ( value ) {
9419         value = this.cleanUpValue( value );
9420         this.checkboxMultiselectWidget.selectItemsByData( value );
9421         OO.ui.CheckboxMultiselectInputWidget.parent.prototype.setValue.call( this, value );
9422         return this;
9423 };
9424
9425 /**
9426  * Clean up incoming value.
9427  *
9428  * @param {string[]} value Original value
9429  * @return {string[]} Cleaned up value
9430  */
9431 OO.ui.CheckboxMultiselectInputWidget.prototype.cleanUpValue = function ( value ) {
9432         var i, singleValue,
9433                 cleanValue = [];
9434         if ( !Array.isArray( value ) ) {
9435                 return cleanValue;
9436         }
9437         for ( i = 0; i < value.length; i++ ) {
9438                 singleValue =
9439                         OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call( this, value[ i ] );
9440                 // Remove options that we don't have here
9441                 if ( !this.checkboxMultiselectWidget.getItemFromData( singleValue ) ) {
9442                         continue;
9443                 }
9444                 cleanValue.push( singleValue );
9445         }
9446         return cleanValue;
9447 };
9448
9449 /**
9450  * @inheritdoc
9451  */
9452 OO.ui.CheckboxMultiselectInputWidget.prototype.setDisabled = function ( state ) {
9453         this.checkboxMultiselectWidget.setDisabled( state );
9454         OO.ui.CheckboxMultiselectInputWidget.parent.prototype.setDisabled.call( this, state );
9455         return this;
9456 };
9457
9458 /**
9459  * Set the options available for this input.
9460  *
9461  * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
9462  * @chainable
9463  */
9464 OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options ) {
9465         var widget = this;
9466
9467         // Rebuild the checkboxMultiselectWidget menu
9468         this.checkboxMultiselectWidget
9469                 .clearItems()
9470                 .addItems( options.map( function ( opt ) {
9471                         var optValue, item, optDisabled;
9472                         optValue =
9473                                 OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call( widget, opt.data );
9474                         optDisabled = opt.disabled !== undefined ? opt.disabled : false;
9475                         item = new OO.ui.CheckboxMultioptionWidget( {
9476                                 data: optValue,
9477                                 label: opt.label !== undefined ? opt.label : optValue,
9478                                 disabled: optDisabled
9479                         } );
9480                         // Set the 'name' and 'value' for form submission
9481                         item.checkbox.$input.attr( 'name', widget.inputName );
9482                         item.checkbox.setValue( optValue );
9483                         return item;
9484                 } ) );
9485
9486         // Re-set the value, checking the checkboxes as needed.
9487         // This will also get rid of any stale options that we just removed.
9488         this.setValue( this.getValue() );
9489
9490         return this;
9491 };
9492
9493 /**
9494  * @inheritdoc
9495  */
9496 OO.ui.CheckboxMultiselectInputWidget.prototype.focus = function () {
9497         this.checkboxMultiselectWidget.focus();
9498         return this;
9499 };
9500
9501 /**
9502  * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
9503  * size of the field as well as its presentation. In addition, these widgets can be configured
9504  * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
9505  * validation-pattern (used to determine if an input value is valid or not) and an input filter,
9506  * which modifies incoming values rather than validating them.
9507  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9508  *
9509  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9510  *
9511  *     @example
9512  *     // Example of a text input widget
9513  *     var textInput = new OO.ui.TextInputWidget( {
9514  *         value: 'Text input'
9515  *     } )
9516  *     $( 'body' ).append( textInput.$element );
9517  *
9518  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9519  *
9520  * @class
9521  * @extends OO.ui.InputWidget
9522  * @mixins OO.ui.mixin.IconElement
9523  * @mixins OO.ui.mixin.IndicatorElement
9524  * @mixins OO.ui.mixin.PendingElement
9525  * @mixins OO.ui.mixin.LabelElement
9526  *
9527  * @constructor
9528  * @param {Object} [config] Configuration options
9529  * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
9530  *  'email', 'url' or 'number'.
9531  * @cfg {string} [placeholder] Placeholder text
9532  * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
9533  *  instruct the browser to focus this widget.
9534  * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
9535  * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
9536  * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
9537  *  the value or placeholder text: `'before'` or `'after'`
9538  * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
9539  * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
9540  * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
9541  *  pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
9542  *  (the value must contain only numbers); when RegExp, a regular expression that must match the
9543  *  value for it to be considered valid; when Function, a function receiving the value as parameter
9544  *  that must return true, or promise resolving to true, for it to be considered valid.
9545  */
9546 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
9547         // Configuration initialization
9548         config = $.extend( {
9549                 type: 'text',
9550                 labelPosition: 'after'
9551         }, config );
9552
9553         if ( config.multiline ) {
9554                 OO.ui.warnDeprecation( 'TextInputWidget: config.multiline is deprecated. Use the MultilineTextInputWidget instead. See T130434.' );
9555                 return new OO.ui.MultilineTextInputWidget( config );
9556         }
9557
9558         // Parent constructor
9559         OO.ui.TextInputWidget.parent.call( this, config );
9560
9561         // Mixin constructors
9562         OO.ui.mixin.IconElement.call( this, config );
9563         OO.ui.mixin.IndicatorElement.call( this, config );
9564         OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
9565         OO.ui.mixin.LabelElement.call( this, config );
9566
9567         // Properties
9568         this.type = this.getSaneType( config );
9569         this.readOnly = false;
9570         this.required = false;
9571         this.validate = null;
9572         this.styleHeight = null;
9573         this.scrollWidth = null;
9574
9575         this.setValidation( config.validate );
9576         this.setLabelPosition( config.labelPosition );
9577
9578         // Events
9579         this.$input.on( {
9580                 keypress: this.onKeyPress.bind( this ),
9581                 blur: this.onBlur.bind( this ),
9582                 focus: this.onFocus.bind( this )
9583         } );
9584         this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
9585         this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
9586         this.on( 'labelChange', this.updatePosition.bind( this ) );
9587         this.on( 'change', OO.ui.debounce( this.onDebouncedChange.bind( this ), 250 ) );
9588
9589         // Initialization
9590         this.$element
9591                 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
9592                 .append( this.$icon, this.$indicator );
9593         this.setReadOnly( !!config.readOnly );
9594         this.setRequired( !!config.required );
9595         if ( config.placeholder !== undefined ) {
9596                 this.$input.attr( 'placeholder', config.placeholder );
9597         }
9598         if ( config.maxLength !== undefined ) {
9599                 this.$input.attr( 'maxlength', config.maxLength );
9600         }
9601         if ( config.autofocus ) {
9602                 this.$input.attr( 'autofocus', 'autofocus' );
9603         }
9604         if ( config.autocomplete === false ) {
9605                 this.$input.attr( 'autocomplete', 'off' );
9606                 // Turning off autocompletion also disables "form caching" when the user navigates to a
9607                 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
9608                 $( window ).on( {
9609                         beforeunload: function () {
9610                                 this.$input.removeAttr( 'autocomplete' );
9611                         }.bind( this ),
9612                         pageshow: function () {
9613                                 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
9614                                 // whole page... it shouldn't hurt, though.
9615                                 this.$input.attr( 'autocomplete', 'off' );
9616                         }.bind( this )
9617                 } );
9618         }
9619         if ( this.label ) {
9620                 this.isWaitingToBeAttached = true;
9621                 this.installParentChangeDetector();
9622         }
9623 };
9624
9625 /* Setup */
9626
9627 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
9628 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
9629 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
9630 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
9631 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
9632
9633 /* Static Properties */
9634
9635 OO.ui.TextInputWidget.static.validationPatterns = {
9636         'non-empty': /.+/,
9637         integer: /^\d+$/
9638 };
9639
9640 /* Static Methods */
9641
9642 /**
9643  * @inheritdoc
9644  */
9645 OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
9646         var state = OO.ui.TextInputWidget.parent.static.gatherPreInfuseState( node, config );
9647         return state;
9648 };
9649
9650 /* Events */
9651
9652 /**
9653  * An `enter` event is emitted when the user presses 'enter' inside the text box.
9654  *
9655  * @event enter
9656  */
9657
9658 /* Methods */
9659
9660 /**
9661  * Handle icon mouse down events.
9662  *
9663  * @private
9664  * @param {jQuery.Event} e Mouse down event
9665  */
9666 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
9667         if ( e.which === OO.ui.MouseButtons.LEFT ) {
9668                 this.focus();
9669                 return false;
9670         }
9671 };
9672
9673 /**
9674  * Handle indicator mouse down events.
9675  *
9676  * @private
9677  * @param {jQuery.Event} e Mouse down event
9678  */
9679 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
9680         if ( e.which === OO.ui.MouseButtons.LEFT ) {
9681                 this.focus();
9682                 return false;
9683         }
9684 };
9685
9686 /**
9687  * Handle key press events.
9688  *
9689  * @private
9690  * @param {jQuery.Event} e Key press event
9691  * @fires enter If enter key is pressed
9692  */
9693 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
9694         if ( e.which === OO.ui.Keys.ENTER ) {
9695                 this.emit( 'enter', e );
9696         }
9697 };
9698
9699 /**
9700  * Handle blur events.
9701  *
9702  * @private
9703  * @param {jQuery.Event} e Blur event
9704  */
9705 OO.ui.TextInputWidget.prototype.onBlur = function () {
9706         this.setValidityFlag();
9707 };
9708
9709 /**
9710  * Handle focus events.
9711  *
9712  * @private
9713  * @param {jQuery.Event} e Focus event
9714  */
9715 OO.ui.TextInputWidget.prototype.onFocus = function () {
9716         if ( this.isWaitingToBeAttached ) {
9717                 // If we've received focus, then we must be attached to the document, and if
9718                 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
9719                 this.onElementAttach();
9720         }
9721         this.setValidityFlag( true );
9722 };
9723
9724 /**
9725  * Handle element attach events.
9726  *
9727  * @private
9728  * @param {jQuery.Event} e Element attach event
9729  */
9730 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
9731         this.isWaitingToBeAttached = false;
9732         // Any previously calculated size is now probably invalid if we reattached elsewhere
9733         this.valCache = null;
9734         this.positionLabel();
9735 };
9736
9737 /**
9738  * Handle debounced change events.
9739  *
9740  * @param {string} value
9741  * @private
9742  */
9743 OO.ui.TextInputWidget.prototype.onDebouncedChange = function () {
9744         this.setValidityFlag();
9745 };
9746
9747 /**
9748  * Check if the input is {@link #readOnly read-only}.
9749  *
9750  * @return {boolean}
9751  */
9752 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
9753         return this.readOnly;
9754 };
9755
9756 /**
9757  * Set the {@link #readOnly read-only} state of the input.
9758  *
9759  * @param {boolean} state Make input read-only
9760  * @chainable
9761  */
9762 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
9763         this.readOnly = !!state;
9764         this.$input.prop( 'readOnly', this.readOnly );
9765         return this;
9766 };
9767
9768 /**
9769  * Check if the input is {@link #required required}.
9770  *
9771  * @return {boolean}
9772  */
9773 OO.ui.TextInputWidget.prototype.isRequired = function () {
9774         return this.required;
9775 };
9776
9777 /**
9778  * Set the {@link #required required} state of the input.
9779  *
9780  * @param {boolean} state Make input required
9781  * @chainable
9782  */
9783 OO.ui.TextInputWidget.prototype.setRequired = function ( state ) {
9784         this.required = !!state;
9785         if ( this.required ) {
9786                 this.$input
9787                         .prop( 'required', true )
9788                         .attr( 'aria-required', 'true' );
9789                 if ( this.getIndicator() === null ) {
9790                         this.setIndicator( 'required' );
9791                 }
9792         } else {
9793                 this.$input
9794                         .prop( 'required', false )
9795                         .removeAttr( 'aria-required' );
9796                 if ( this.getIndicator() === 'required' ) {
9797                         this.setIndicator( null );
9798                 }
9799         }
9800         return this;
9801 };
9802
9803 /**
9804  * Support function for making #onElementAttach work across browsers.
9805  *
9806  * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
9807  * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
9808  *
9809  * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
9810  * first time that the element gets attached to the documented.
9811  */
9812 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
9813         var mutationObserver, onRemove, topmostNode, fakeParentNode,
9814                 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
9815                 widget = this;
9816
9817         if ( MutationObserver ) {
9818                 // The new way. If only it wasn't so ugly.
9819
9820                 if ( this.isElementAttached() ) {
9821                         // Widget is attached already, do nothing. This breaks the functionality of this function when
9822                         // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
9823                         // would require observation of the whole document, which would hurt performance of other,
9824                         // more important code.
9825                         return;
9826                 }
9827
9828                 // Find topmost node in the tree
9829                 topmostNode = this.$element[ 0 ];
9830                 while ( topmostNode.parentNode ) {
9831                         topmostNode = topmostNode.parentNode;
9832                 }
9833
9834                 // We have no way to detect the $element being attached somewhere without observing the entire
9835                 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
9836                 // parent node of $element, and instead detect when $element is removed from it (and thus
9837                 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
9838                 // doesn't get attached, we end up back here and create the parent.
9839
9840                 mutationObserver = new MutationObserver( function ( mutations ) {
9841                         var i, j, removedNodes;
9842                         for ( i = 0; i < mutations.length; i++ ) {
9843                                 removedNodes = mutations[ i ].removedNodes;
9844                                 for ( j = 0; j < removedNodes.length; j++ ) {
9845                                         if ( removedNodes[ j ] === topmostNode ) {
9846                                                 setTimeout( onRemove, 0 );
9847                                                 return;
9848                                         }
9849                                 }
9850                         }
9851                 } );
9852
9853                 onRemove = function () {
9854                         // If the node was attached somewhere else, report it
9855                         if ( widget.isElementAttached() ) {
9856                                 widget.onElementAttach();
9857                         }
9858                         mutationObserver.disconnect();
9859                         widget.installParentChangeDetector();
9860                 };
9861
9862                 // Create a fake parent and observe it
9863                 fakeParentNode = $( '<div>' ).append( topmostNode )[ 0 ];
9864                 mutationObserver.observe( fakeParentNode, { childList: true } );
9865         } else {
9866                 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
9867                 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
9868                 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
9869         }
9870 };
9871
9872 /**
9873  * @inheritdoc
9874  * @protected
9875  */
9876 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
9877         if ( this.getSaneType( config ) === 'number' ) {
9878                 return $( '<input>' )
9879                         .attr( 'step', 'any' )
9880                         .attr( 'type', 'number' );
9881         } else {
9882                 return $( '<input>' ).attr( 'type', this.getSaneType( config ) );
9883         }
9884 };
9885
9886 /**
9887  * Get sanitized value for 'type' for given config.
9888  *
9889  * @param {Object} config Configuration options
9890  * @return {string|null}
9891  * @private
9892  */
9893 OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
9894         var allowedTypes = [
9895                 'text',
9896                 'password',
9897                 'email',
9898                 'url',
9899                 'number'
9900         ];
9901         return allowedTypes.indexOf( config.type ) !== -1 ? config.type : 'text';
9902 };
9903
9904 /**
9905  * Focus the input and select a specified range within the text.
9906  *
9907  * @param {number} from Select from offset
9908  * @param {number} [to] Select to offset, defaults to from
9909  * @chainable
9910  */
9911 OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
9912         var isBackwards, start, end,
9913                 input = this.$input[ 0 ];
9914
9915         to = to || from;
9916
9917         isBackwards = to < from;
9918         start = isBackwards ? to : from;
9919         end = isBackwards ? from : to;
9920
9921         this.focus();
9922
9923         try {
9924                 input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
9925         } catch ( e ) {
9926                 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
9927                 // Rather than expensively check if the input is attached every time, just check
9928                 // if it was the cause of an error being thrown. If not, rethrow the error.
9929                 if ( this.getElementDocument().body.contains( input ) ) {
9930                         throw e;
9931                 }
9932         }
9933         return this;
9934 };
9935
9936 /**
9937  * Get an object describing the current selection range in a directional manner
9938  *
9939  * @return {Object} Object containing 'from' and 'to' offsets
9940  */
9941 OO.ui.TextInputWidget.prototype.getRange = function () {
9942         var input = this.$input[ 0 ],
9943                 start = input.selectionStart,
9944                 end = input.selectionEnd,
9945                 isBackwards = input.selectionDirection === 'backward';
9946
9947         return {
9948                 from: isBackwards ? end : start,
9949                 to: isBackwards ? start : end
9950         };
9951 };
9952
9953 /**
9954  * Get the length of the text input value.
9955  *
9956  * This could differ from the length of #getValue if the
9957  * value gets filtered
9958  *
9959  * @return {number} Input length
9960  */
9961 OO.ui.TextInputWidget.prototype.getInputLength = function () {
9962         return this.$input[ 0 ].value.length;
9963 };
9964
9965 /**
9966  * Focus the input and select the entire text.
9967  *
9968  * @chainable
9969  */
9970 OO.ui.TextInputWidget.prototype.select = function () {
9971         return this.selectRange( 0, this.getInputLength() );
9972 };
9973
9974 /**
9975  * Focus the input and move the cursor to the start.
9976  *
9977  * @chainable
9978  */
9979 OO.ui.TextInputWidget.prototype.moveCursorToStart = function () {
9980         return this.selectRange( 0 );
9981 };
9982
9983 /**
9984  * Focus the input and move the cursor to the end.
9985  *
9986  * @chainable
9987  */
9988 OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
9989         return this.selectRange( this.getInputLength() );
9990 };
9991
9992 /**
9993  * Insert new content into the input.
9994  *
9995  * @param {string} content Content to be inserted
9996  * @chainable
9997  */
9998 OO.ui.TextInputWidget.prototype.insertContent = function ( content ) {
9999         var start, end,
10000                 range = this.getRange(),
10001                 value = this.getValue();
10002
10003         start = Math.min( range.from, range.to );
10004         end = Math.max( range.from, range.to );
10005
10006         this.setValue( value.slice( 0, start ) + content + value.slice( end ) );
10007         this.selectRange( start + content.length );
10008         return this;
10009 };
10010
10011 /**
10012  * Insert new content either side of a selection.
10013  *
10014  * @param {string} pre Content to be inserted before the selection
10015  * @param {string} post Content to be inserted after the selection
10016  * @chainable
10017  */
10018 OO.ui.TextInputWidget.prototype.encapsulateContent = function ( pre, post ) {
10019         var start, end,
10020                 range = this.getRange(),
10021                 offset = pre.length;
10022
10023         start = Math.min( range.from, range.to );
10024         end = Math.max( range.from, range.to );
10025
10026         this.selectRange( start ).insertContent( pre );
10027         this.selectRange( offset + end ).insertContent( post );
10028
10029         this.selectRange( offset + start, offset + end );
10030         return this;
10031 };
10032
10033 /**
10034  * Set the validation pattern.
10035  *
10036  * The validation pattern is either a regular expression, a function, or the symbolic name of a
10037  * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10038  * value must contain only numbers).
10039  *
10040  * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10041  *  of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10042  */
10043 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
10044         if ( validate instanceof RegExp || validate instanceof Function ) {
10045                 this.validate = validate;
10046         } else {
10047                 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
10048         }
10049 };
10050
10051 /**
10052  * Sets the 'invalid' flag appropriately.
10053  *
10054  * @param {boolean} [isValid] Optionally override validation result
10055  */
10056 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
10057         var widget = this,
10058                 setFlag = function ( valid ) {
10059                         if ( !valid ) {
10060                                 widget.$input.attr( 'aria-invalid', 'true' );
10061                         } else {
10062                                 widget.$input.removeAttr( 'aria-invalid' );
10063                         }
10064                         widget.setFlags( { invalid: !valid } );
10065                 };
10066
10067         if ( isValid !== undefined ) {
10068                 setFlag( isValid );
10069         } else {
10070                 this.getValidity().then( function () {
10071                         setFlag( true );
10072                 }, function () {
10073                         setFlag( false );
10074                 } );
10075         }
10076 };
10077
10078 /**
10079  * Get the validity of current value.
10080  *
10081  * This method returns a promise that resolves if the value is valid and rejects if
10082  * it isn't. Uses the {@link #validate validation pattern}  to check for validity.
10083  *
10084  * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10085  */
10086 OO.ui.TextInputWidget.prototype.getValidity = function () {
10087         var result;
10088
10089         function rejectOrResolve( valid ) {
10090                 if ( valid ) {
10091                         return $.Deferred().resolve().promise();
10092                 } else {
10093                         return $.Deferred().reject().promise();
10094                 }
10095         }
10096
10097         // Check browser validity and reject if it is invalid
10098         if (
10099                 this.$input[ 0 ].checkValidity !== undefined &&
10100                 this.$input[ 0 ].checkValidity() === false
10101         ) {
10102                 return rejectOrResolve( false );
10103         }
10104
10105         // Run our checks if the browser thinks the field is valid
10106         if ( this.validate instanceof Function ) {
10107                 result = this.validate( this.getValue() );
10108                 if ( result && $.isFunction( result.promise ) ) {
10109                         return result.promise().then( function ( valid ) {
10110                                 return rejectOrResolve( valid );
10111                         } );
10112                 } else {
10113                         return rejectOrResolve( result );
10114                 }
10115         } else {
10116                 return rejectOrResolve( this.getValue().match( this.validate ) );
10117         }
10118 };
10119
10120 /**
10121  * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10122  *
10123  * @param {string} labelPosition Label position, 'before' or 'after'
10124  * @chainable
10125  */
10126 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
10127         this.labelPosition = labelPosition;
10128         if ( this.label ) {
10129                 // If there is no label and we only change the position, #updatePosition is a no-op,
10130                 // but it takes really a lot of work to do nothing.
10131                 this.updatePosition();
10132         }
10133         return this;
10134 };
10135
10136 /**
10137  * Update the position of the inline label.
10138  *
10139  * This method is called by #setLabelPosition, and can also be called on its own if
10140  * something causes the label to be mispositioned.
10141  *
10142  * @chainable
10143  */
10144 OO.ui.TextInputWidget.prototype.updatePosition = function () {
10145         var after = this.labelPosition === 'after';
10146
10147         this.$element
10148                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
10149                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
10150
10151         this.valCache = null;
10152         this.scrollWidth = null;
10153         this.positionLabel();
10154
10155         return this;
10156 };
10157
10158 /**
10159  * Position the label by setting the correct padding on the input.
10160  *
10161  * @private
10162  * @chainable
10163  */
10164 OO.ui.TextInputWidget.prototype.positionLabel = function () {
10165         var after, rtl, property, newCss;
10166
10167         if ( this.isWaitingToBeAttached ) {
10168                 // #onElementAttach will be called soon, which calls this method
10169                 return this;
10170         }
10171
10172         newCss = {
10173                 'padding-right': '',
10174                 'padding-left': ''
10175         };
10176
10177         if ( this.label ) {
10178                 this.$element.append( this.$label );
10179         } else {
10180                 this.$label.detach();
10181                 // Clear old values if present
10182                 this.$input.css( newCss );
10183                 return;
10184         }
10185
10186         after = this.labelPosition === 'after';
10187         rtl = this.$element.css( 'direction' ) === 'rtl';
10188         property = after === rtl ? 'padding-left' : 'padding-right';
10189
10190         newCss[ property ] = this.$label.outerWidth( true ) + ( after ? this.scrollWidth : 0 );
10191         // We have to clear the padding on the other side, in case the element direction changed
10192         this.$input.css( newCss );
10193
10194         return this;
10195 };
10196
10197 /**
10198  * @inheritdoc
10199  */
10200 OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
10201         OO.ui.TextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
10202         if ( state.scrollTop !== undefined ) {
10203                 this.$input.scrollTop( state.scrollTop );
10204         }
10205 };
10206
10207 /**
10208  * @class
10209  * @extends OO.ui.TextInputWidget
10210  *
10211  * @constructor
10212  * @param {Object} [config] Configuration options
10213  */
10214 OO.ui.SearchInputWidget = function OoUiSearchInputWidget( config ) {
10215         config = $.extend( {
10216                 icon: 'search'
10217         }, config );
10218
10219         // Set type to text so that TextInputWidget doesn't
10220         // get stuck in an infinite loop.
10221         config.type = 'text';
10222
10223         // Parent constructor
10224         OO.ui.SearchInputWidget.parent.call( this, config );
10225
10226         // Events
10227         this.connect( this, {
10228                 change: 'onChange'
10229         } );
10230
10231         // Initialization
10232         this.$element.addClass( 'oo-ui-textInputWidget-type-search' );
10233         this.updateSearchIndicator();
10234         this.connect( this, {
10235                 disable: 'onDisable'
10236         } );
10237 };
10238
10239 /* Setup */
10240
10241 OO.inheritClass( OO.ui.SearchInputWidget, OO.ui.TextInputWidget );
10242
10243 /* Methods */
10244
10245 /**
10246  * @inheritdoc
10247  * @protected
10248  */
10249 OO.ui.SearchInputWidget.prototype.getInputElement = function () {
10250         return $( '<input>' ).attr( 'type', 'search' );
10251 };
10252
10253 /**
10254  * @inheritdoc
10255  */
10256 OO.ui.SearchInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
10257         if ( e.which === OO.ui.MouseButtons.LEFT ) {
10258                 // Clear the text field
10259                 this.setValue( '' );
10260                 this.focus();
10261                 return false;
10262         }
10263 };
10264
10265 /**
10266  * Update the 'clear' indicator displayed on type: 'search' text
10267  * fields, hiding it when the field is already empty or when it's not
10268  * editable.
10269  */
10270 OO.ui.SearchInputWidget.prototype.updateSearchIndicator = function () {
10271         if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
10272                 this.setIndicator( null );
10273         } else {
10274                 this.setIndicator( 'clear' );
10275         }
10276 };
10277
10278 /**
10279  * Handle change events.
10280  *
10281  * @private
10282  */
10283 OO.ui.SearchInputWidget.prototype.onChange = function () {
10284         this.updateSearchIndicator();
10285 };
10286
10287 /**
10288  * Handle disable events.
10289  *
10290  * @param {boolean} disabled Element is disabled
10291  * @private
10292  */
10293 OO.ui.SearchInputWidget.prototype.onDisable = function () {
10294         this.updateSearchIndicator();
10295 };
10296
10297 /**
10298  * @inheritdoc
10299  */
10300 OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) {
10301         OO.ui.SearchInputWidget.parent.prototype.setReadOnly.call( this, state );
10302         this.updateSearchIndicator();
10303         return this;
10304 };
10305
10306 /**
10307  * @class
10308  * @extends OO.ui.TextInputWidget
10309  *
10310  * @constructor
10311  * @param {Object} [config] Configuration options
10312  * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
10313  *  specifies minimum number of rows to display.
10314  * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10315  * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
10316  *  Use the #maxRows config to specify a maximum number of displayed rows.
10317  * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
10318  *  Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
10319  */
10320 OO.ui.MultilineTextInputWidget = function OoUiMultilineTextInputWidget( config ) {
10321         config = $.extend( {
10322                 type: 'text'
10323         }, config );
10324         config.multiline = false;
10325         // Parent constructor
10326         OO.ui.MultilineTextInputWidget.parent.call( this, config );
10327
10328         // Properties
10329         this.multiline = true;
10330         this.autosize = !!config.autosize;
10331         this.minRows = config.rows !== undefined ? config.rows : '';
10332         this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
10333
10334         // Clone for resizing
10335         if ( this.autosize ) {
10336                 this.$clone = this.$input
10337                         .clone()
10338                         .insertAfter( this.$input )
10339                         .attr( 'aria-hidden', 'true' )
10340                         .addClass( 'oo-ui-element-hidden' );
10341         }
10342
10343         // Events
10344         this.connect( this, {
10345                 change: 'onChange'
10346         } );
10347
10348         // Initialization
10349         if ( this.multiline && config.rows ) {
10350                 this.$input.attr( 'rows', config.rows );
10351         }
10352         if ( this.autosize ) {
10353                 this.isWaitingToBeAttached = true;
10354                 this.installParentChangeDetector();
10355         }
10356 };
10357
10358 /* Setup */
10359
10360 OO.inheritClass( OO.ui.MultilineTextInputWidget, OO.ui.TextInputWidget );
10361
10362 /* Static Methods */
10363
10364 /**
10365  * @inheritdoc
10366  */
10367 OO.ui.MultilineTextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
10368         var state = OO.ui.MultilineTextInputWidget.parent.static.gatherPreInfuseState( node, config );
10369         state.scrollTop = config.$input.scrollTop();
10370         return state;
10371 };
10372
10373 /* Methods */
10374
10375 /**
10376  * @inheritdoc
10377  */
10378 OO.ui.MultilineTextInputWidget.prototype.onElementAttach = function () {
10379         OO.ui.MultilineTextInputWidget.parent.prototype.onElementAttach.call( this );
10380         this.adjustSize();
10381 };
10382
10383 /**
10384  * Handle change events.
10385  *
10386  * @private
10387  */
10388 OO.ui.MultilineTextInputWidget.prototype.onChange = function () {
10389         this.adjustSize();
10390 };
10391
10392 /**
10393  * @inheritdoc
10394  */
10395 OO.ui.MultilineTextInputWidget.prototype.updatePosition = function () {
10396         OO.ui.MultilineTextInputWidget.parent.prototype.updatePosition.call( this );
10397         this.adjustSize();
10398 };
10399
10400 /**
10401  * Override TextInputWidget so it doesn't emit the 'enter' event.
10402  *
10403  * @private
10404  * @param {jQuery.Event} e Key press event
10405  */
10406 OO.ui.MultilineTextInputWidget.prototype.onKeyPress = function () {
10407         return;
10408 };
10409
10410 /**
10411  * Automatically adjust the size of the text input.
10412  *
10413  * This only affects multiline inputs that are {@link #autosize autosized}.
10414  *
10415  * @chainable
10416  * @fires resize
10417  */
10418 OO.ui.MultilineTextInputWidget.prototype.adjustSize = function () {
10419         var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError,
10420                 idealHeight, newHeight, scrollWidth, property;
10421
10422         if ( this.$input.val() !== this.valCache ) {
10423                 if ( this.autosize ) {
10424                         this.$clone
10425                                 .val( this.$input.val() )
10426                                 .attr( 'rows', this.minRows )
10427                                 // Set inline height property to 0 to measure scroll height
10428                                 .css( 'height', 0 );
10429
10430                         this.$clone.removeClass( 'oo-ui-element-hidden' );
10431
10432                         this.valCache = this.$input.val();
10433
10434                         scrollHeight = this.$clone[ 0 ].scrollHeight;
10435
10436                         // Remove inline height property to measure natural heights
10437                         this.$clone.css( 'height', '' );
10438                         innerHeight = this.$clone.innerHeight();
10439                         outerHeight = this.$clone.outerHeight();
10440
10441                         // Measure max rows height
10442                         this.$clone
10443                                 .attr( 'rows', this.maxRows )
10444                                 .css( 'height', 'auto' )
10445                                 .val( '' );
10446                         maxInnerHeight = this.$clone.innerHeight();
10447
10448                         // Difference between reported innerHeight and scrollHeight with no scrollbars present.
10449                         // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
10450                         measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
10451                         idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
10452
10453                         this.$clone.addClass( 'oo-ui-element-hidden' );
10454
10455                         // Only apply inline height when expansion beyond natural height is needed
10456                         // Use the difference between the inner and outer height as a buffer
10457                         newHeight = idealHeight > innerHeight ? idealHeight + ( outerHeight - innerHeight ) : '';
10458                         if ( newHeight !== this.styleHeight ) {
10459                                 this.$input.css( 'height', newHeight );
10460                                 this.styleHeight = newHeight;
10461                                 this.emit( 'resize' );
10462                         }
10463                 }
10464                 scrollWidth = this.$input[ 0 ].offsetWidth - this.$input[ 0 ].clientWidth;
10465                 if ( scrollWidth !== this.scrollWidth ) {
10466                         property = this.$element.css( 'direction' ) === 'rtl' ? 'left' : 'right';
10467                         // Reset
10468                         this.$label.css( { right: '', left: '' } );
10469                         this.$indicator.css( { right: '', left: '' } );
10470
10471                         if ( scrollWidth ) {
10472                                 this.$indicator.css( property, scrollWidth );
10473                                 if ( this.labelPosition === 'after' ) {
10474                                         this.$label.css( property, scrollWidth );
10475                                 }
10476                         }
10477
10478                         this.scrollWidth = scrollWidth;
10479                         this.positionLabel();
10480                 }
10481         }
10482         return this;
10483 };
10484
10485 /**
10486  * @inheritdoc
10487  * @protected
10488  */
10489 OO.ui.MultilineTextInputWidget.prototype.getInputElement = function () {
10490         return $( '<textarea>' );
10491 };
10492
10493 /**
10494  * Check if the input supports multiple lines.
10495  *
10496  * @return {boolean}
10497  */
10498 OO.ui.MultilineTextInputWidget.prototype.isMultiline = function () {
10499         return !!this.multiline;
10500 };
10501
10502 /**
10503  * Check if the input automatically adjusts its size.
10504  *
10505  * @return {boolean}
10506  */
10507 OO.ui.MultilineTextInputWidget.prototype.isAutosizing = function () {
10508         return !!this.autosize;
10509 };
10510
10511 /**
10512  * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
10513  * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
10514  * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
10515  *
10516  * - by typing a value in the text input field. If the value exactly matches the value of a menu
10517  *   option, that option will appear to be selected.
10518  * - by choosing a value from the menu. The value of the chosen option will then appear in the text
10519  *   input field.
10520  *
10521  * After the user chooses an option, its `data` will be used as a new value for the widget.
10522  * A `label` also can be specified for each option: if given, it will be shown instead of the
10523  * `data` in the dropdown menu.
10524  *
10525  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10526  *
10527  * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
10528  *
10529  *     @example
10530  *     // Example: A ComboBoxInputWidget.
10531  *     var comboBox = new OO.ui.ComboBoxInputWidget( {
10532  *         value: 'Option 1',
10533  *         options: [
10534  *             { data: 'Option 1' },
10535  *             { data: 'Option 2' },
10536  *             { data: 'Option 3' }
10537  *         ]
10538  *     } );
10539  *     $( 'body' ).append( comboBox.$element );
10540  *
10541  *     @example
10542  *     // Example: A ComboBoxInputWidget with additional option labels.
10543  *     var comboBox = new OO.ui.ComboBoxInputWidget( {
10544  *         value: 'Option 1',
10545  *         options: [
10546  *             {
10547  *                 data: 'Option 1',
10548  *                 label: 'Option One'
10549  *             },
10550  *             {
10551  *                 data: 'Option 2',
10552  *                 label: 'Option Two'
10553  *             },
10554  *             {
10555  *                 data: 'Option 3',
10556  *                 label: 'Option Three'
10557  *             }
10558  *         ]
10559  *     } );
10560  *     $( 'body' ).append( comboBox.$element );
10561  *
10562  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10563  *
10564  * @class
10565  * @extends OO.ui.TextInputWidget
10566  *
10567  * @constructor
10568  * @param {Object} [config] Configuration options
10569  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10570  * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
10571  * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
10572  *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
10573  *  containing `<div>` and has a larger area. By default, the menu uses relative positioning.
10574  *  See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
10575  */
10576 OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
10577         // Configuration initialization
10578         config = $.extend( {
10579                 autocomplete: false
10580         }, config );
10581
10582         // ComboBoxInputWidget shouldn't support `multiline`
10583         config.multiline = false;
10584
10585         // See InputWidget#reusePreInfuseDOM about `config.$input`
10586         if ( config.$input ) {
10587                 config.$input.removeAttr( 'list' );
10588         }
10589
10590         // Parent constructor
10591         OO.ui.ComboBoxInputWidget.parent.call( this, config );
10592
10593         // Properties
10594         this.$overlay = config.$overlay || this.$element;
10595         this.dropdownButton = new OO.ui.ButtonWidget( {
10596                 classes: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
10597                 indicator: 'down',
10598                 disabled: this.disabled
10599         } );
10600         this.menu = new OO.ui.MenuSelectWidget( $.extend(
10601                 {
10602                         widget: this,
10603                         input: this,
10604                         $floatableContainer: this.$element,
10605                         disabled: this.isDisabled()
10606                 },
10607                 config.menu
10608         ) );
10609
10610         // Events
10611         this.connect( this, {
10612                 change: 'onInputChange',
10613                 enter: 'onInputEnter'
10614         } );
10615         this.dropdownButton.connect( this, {
10616                 click: 'onDropdownButtonClick'
10617         } );
10618         this.menu.connect( this, {
10619                 choose: 'onMenuChoose',
10620                 add: 'onMenuItemsChange',
10621                 remove: 'onMenuItemsChange'
10622         } );
10623
10624         // Initialization
10625         this.$input.attr( {
10626                 role: 'combobox',
10627                 'aria-owns': this.menu.getElementId(),
10628                 'aria-autocomplete': 'list'
10629         } );
10630         // Do not override options set via config.menu.items
10631         if ( config.options !== undefined ) {
10632                 this.setOptions( config.options );
10633         }
10634         this.$field = $( '<div>' )
10635                 .addClass( 'oo-ui-comboBoxInputWidget-field' )
10636                 .append( this.$input, this.dropdownButton.$element );
10637         this.$element
10638                 .addClass( 'oo-ui-comboBoxInputWidget' )
10639                 .append( this.$field );
10640         this.$overlay.append( this.menu.$element );
10641         this.onMenuItemsChange();
10642 };
10643
10644 /* Setup */
10645
10646 OO.inheritClass( OO.ui.ComboBoxInputWidget, OO.ui.TextInputWidget );
10647
10648 /* Methods */
10649
10650 /**
10651  * Get the combobox's menu.
10652  *
10653  * @return {OO.ui.MenuSelectWidget} Menu widget
10654  */
10655 OO.ui.ComboBoxInputWidget.prototype.getMenu = function () {
10656         return this.menu;
10657 };
10658
10659 /**
10660  * Get the combobox's text input widget.
10661  *
10662  * @return {OO.ui.TextInputWidget} Text input widget
10663  */
10664 OO.ui.ComboBoxInputWidget.prototype.getInput = function () {
10665         return this;
10666 };
10667
10668 /**
10669  * Handle input change events.
10670  *
10671  * @private
10672  * @param {string} value New value
10673  */
10674 OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
10675         var match = this.menu.getItemFromData( value );
10676
10677         this.menu.selectItem( match );
10678         if ( this.menu.getHighlightedItem() ) {
10679                 this.menu.highlightItem( match );
10680         }
10681
10682         if ( !this.isDisabled() ) {
10683                 this.menu.toggle( true );
10684         }
10685 };
10686
10687 /**
10688  * Handle input enter events.
10689  *
10690  * @private
10691  */
10692 OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
10693         if ( !this.isDisabled() ) {
10694                 this.menu.toggle( false );
10695         }
10696 };
10697
10698 /**
10699  * Handle button click events.
10700  *
10701  * @private
10702  */
10703 OO.ui.ComboBoxInputWidget.prototype.onDropdownButtonClick = function () {
10704         this.menu.toggle();
10705         this.focus();
10706 };
10707
10708 /**
10709  * Handle menu choose events.
10710  *
10711  * @private
10712  * @param {OO.ui.OptionWidget} item Chosen item
10713  */
10714 OO.ui.ComboBoxInputWidget.prototype.onMenuChoose = function ( item ) {
10715         this.setValue( item.getData() );
10716 };
10717
10718 /**
10719  * Handle menu item change events.
10720  *
10721  * @private
10722  */
10723 OO.ui.ComboBoxInputWidget.prototype.onMenuItemsChange = function () {
10724         var match = this.menu.getItemFromData( this.getValue() );
10725         this.menu.selectItem( match );
10726         if ( this.menu.getHighlightedItem() ) {
10727                 this.menu.highlightItem( match );
10728         }
10729         this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu.isEmpty() );
10730 };
10731
10732 /**
10733  * @inheritdoc
10734  */
10735 OO.ui.ComboBoxInputWidget.prototype.setDisabled = function ( disabled ) {
10736         // Parent method
10737         OO.ui.ComboBoxInputWidget.parent.prototype.setDisabled.call( this, disabled );
10738
10739         if ( this.dropdownButton ) {
10740                 this.dropdownButton.setDisabled( this.isDisabled() );
10741         }
10742         if ( this.menu ) {
10743                 this.menu.setDisabled( this.isDisabled() );
10744         }
10745
10746         return this;
10747 };
10748
10749 /**
10750  * Set the options available for this input.
10751  *
10752  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10753  * @chainable
10754  */
10755 OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
10756         this.getMenu()
10757                 .clearItems()
10758                 .addItems( options.map( function ( opt ) {
10759                         return new OO.ui.MenuOptionWidget( {
10760                                 data: opt.data,
10761                                 label: opt.label !== undefined ? opt.label : opt.data
10762                         } );
10763                 } ) );
10764
10765         return this;
10766 };
10767
10768 /**
10769  * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
10770  * which is a widget that is specified by reference before any optional configuration settings.
10771  *
10772  * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
10773  *
10774  * - **left**: The label is placed before the field-widget and aligned with the left margin.
10775  *   A left-alignment is used for forms with many fields.
10776  * - **right**: The label is placed before the field-widget and aligned to the right margin.
10777  *   A right-alignment is used for long but familiar forms which users tab through,
10778  *   verifying the current field with a quick glance at the label.
10779  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10780  *   that users fill out from top to bottom.
10781  * - **inline**: The label is placed after the field-widget and aligned to the left.
10782  *   An inline-alignment is best used with checkboxes or radio buttons.
10783  *
10784  * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
10785  * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
10786  *
10787  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10788  *
10789  * @class
10790  * @extends OO.ui.Layout
10791  * @mixins OO.ui.mixin.LabelElement
10792  * @mixins OO.ui.mixin.TitledElement
10793  *
10794  * @constructor
10795  * @param {OO.ui.Widget} fieldWidget Field widget
10796  * @param {Object} [config] Configuration options
10797  * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
10798  * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
10799  *  The array may contain strings or OO.ui.HtmlSnippet instances.
10800  * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
10801  *  The array may contain strings or OO.ui.HtmlSnippet instances.
10802  * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
10803  *  in the upper-right corner of the rendered field; clicking it will display the text in a popup.
10804  *  For important messages, you are advised to use `notices`, as they are always shown.
10805  * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
10806  *  See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
10807  *
10808  * @throws {Error} An error is thrown if no widget is specified
10809  */
10810 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
10811         // Allow passing positional parameters inside the config object
10812         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
10813                 config = fieldWidget;
10814                 fieldWidget = config.fieldWidget;
10815         }
10816
10817         // Make sure we have required constructor arguments
10818         if ( fieldWidget === undefined ) {
10819                 throw new Error( 'Widget not found' );
10820         }
10821
10822         // Configuration initialization
10823         config = $.extend( { align: 'left' }, config );
10824
10825         // Parent constructor
10826         OO.ui.FieldLayout.parent.call( this, config );
10827
10828         // Mixin constructors
10829         OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
10830                 $label: $( '<label>' )
10831         } ) );
10832         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
10833
10834         // Properties
10835         this.fieldWidget = fieldWidget;
10836         this.errors = [];
10837         this.notices = [];
10838         this.$field = this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
10839         this.$messages = $( '<ul>' );
10840         this.$header = $( '<span>' );
10841         this.$body = $( '<div>' );
10842         this.align = null;
10843         if ( config.help ) {
10844                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
10845                         $overlay: config.$overlay,
10846                         popup: {
10847                                 padded: true
10848                         },
10849                         classes: [ 'oo-ui-fieldLayout-help' ],
10850                         framed: false,
10851                         icon: 'info'
10852                 } );
10853                 if ( config.help instanceof OO.ui.HtmlSnippet ) {
10854                         this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
10855                 } else {
10856                         this.popupButtonWidget.getPopup().$body.text( config.help );
10857                 }
10858                 this.$help = this.popupButtonWidget.$element;
10859         } else {
10860                 this.$help = $( [] );
10861         }
10862
10863         // Events
10864         this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
10865
10866         // Initialization
10867         if ( config.help ) {
10868                 // Set the 'aria-describedby' attribute on the fieldWidget
10869                 // Preference given to an input or a button
10870                 (
10871                         this.fieldWidget.$input ||
10872                         this.fieldWidget.$button ||
10873                         this.fieldWidget.$element
10874                 ).attr(
10875                         'aria-describedby',
10876                         this.popupButtonWidget.getPopup().getBodyId()
10877                 );
10878         }
10879         if ( this.fieldWidget.getInputId() ) {
10880                 this.$label.attr( 'for', this.fieldWidget.getInputId() );
10881         } else {
10882                 this.$label.on( 'click', function () {
10883                         this.fieldWidget.simulateLabelClick();
10884                         return false;
10885                 }.bind( this ) );
10886         }
10887         this.$element
10888                 .addClass( 'oo-ui-fieldLayout' )
10889                 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget.isDisabled() )
10890                 .append( this.$body );
10891         this.$body.addClass( 'oo-ui-fieldLayout-body' );
10892         this.$header.addClass( 'oo-ui-fieldLayout-header' );
10893         this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
10894         this.$field
10895                 .addClass( 'oo-ui-fieldLayout-field' )
10896                 .append( this.fieldWidget.$element );
10897
10898         this.setErrors( config.errors || [] );
10899         this.setNotices( config.notices || [] );
10900         this.setAlignment( config.align );
10901         // Call this again to take into account the widget's accessKey
10902         this.updateTitle();
10903 };
10904
10905 /* Setup */
10906
10907 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
10908 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
10909 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.TitledElement );
10910
10911 /* Methods */
10912
10913 /**
10914  * Handle field disable events.
10915  *
10916  * @private
10917  * @param {boolean} value Field is disabled
10918  */
10919 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
10920         this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
10921 };
10922
10923 /**
10924  * Get the widget contained by the field.
10925  *
10926  * @return {OO.ui.Widget} Field widget
10927  */
10928 OO.ui.FieldLayout.prototype.getField = function () {
10929         return this.fieldWidget;
10930 };
10931
10932 /**
10933  * Return `true` if the given field widget can be used with `'inline'` alignment (see
10934  * #setAlignment). Return `false` if it can't or if this can't be determined.
10935  *
10936  * @return {boolean}
10937  */
10938 OO.ui.FieldLayout.prototype.isFieldInline = function () {
10939         // This is very simplistic, but should be good enough.
10940         return this.getField().$element.prop( 'tagName' ).toLowerCase() === 'span';
10941 };
10942
10943 /**
10944  * @protected
10945  * @param {string} kind 'error' or 'notice'
10946  * @param {string|OO.ui.HtmlSnippet} text
10947  * @return {jQuery}
10948  */
10949 OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
10950         var $listItem, $icon, message;
10951         $listItem = $( '<li>' );
10952         if ( kind === 'error' ) {
10953                 $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
10954                 $listItem.attr( 'role', 'alert' );
10955         } else if ( kind === 'notice' ) {
10956                 $icon = new OO.ui.IconWidget( { icon: 'info' } ).$element;
10957         } else {
10958                 $icon = '';
10959         }
10960         message = new OO.ui.LabelWidget( { label: text } );
10961         $listItem
10962                 .append( $icon, message.$element )
10963                 .addClass( 'oo-ui-fieldLayout-messages-' + kind );
10964         return $listItem;
10965 };
10966
10967 /**
10968  * Set the field alignment mode.
10969  *
10970  * @private
10971  * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
10972  * @chainable
10973  */
10974 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
10975         if ( value !== this.align ) {
10976                 // Default to 'left'
10977                 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
10978                         value = 'left';
10979                 }
10980                 // Validate
10981                 if ( value === 'inline' && !this.isFieldInline() ) {
10982                         value = 'top';
10983                 }
10984                 // Reorder elements
10985                 if ( value === 'top' ) {
10986                         this.$header.append( this.$label, this.$help );
10987                         this.$body.append( this.$header, this.$field );
10988                 } else if ( value === 'inline' ) {
10989                         this.$header.append( this.$label, this.$help );
10990                         this.$body.append( this.$field, this.$header );
10991                 } else {
10992                         this.$header.append( this.$label );
10993                         this.$body.append( this.$header, this.$help, this.$field );
10994                 }
10995                 // Set classes. The following classes can be used here:
10996                 // * oo-ui-fieldLayout-align-left
10997                 // * oo-ui-fieldLayout-align-right
10998                 // * oo-ui-fieldLayout-align-top
10999                 // * oo-ui-fieldLayout-align-inline
11000                 if ( this.align ) {
11001                         this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
11002                 }
11003                 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
11004                 this.align = value;
11005         }
11006
11007         return this;
11008 };
11009
11010 /**
11011  * Set the list of error messages.
11012  *
11013  * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11014  *  The array may contain strings or OO.ui.HtmlSnippet instances.
11015  * @chainable
11016  */
11017 OO.ui.FieldLayout.prototype.setErrors = function ( errors ) {
11018         this.errors = errors.slice();
11019         this.updateMessages();
11020         return this;
11021 };
11022
11023 /**
11024  * Set the list of notice messages.
11025  *
11026  * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11027  *  The array may contain strings or OO.ui.HtmlSnippet instances.
11028  * @chainable
11029  */
11030 OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
11031         this.notices = notices.slice();
11032         this.updateMessages();
11033         return this;
11034 };
11035
11036 /**
11037  * Update the rendering of error and notice messages.
11038  *
11039  * @private
11040  */
11041 OO.ui.FieldLayout.prototype.updateMessages = function () {
11042         var i;
11043         this.$messages.empty();
11044
11045         if ( this.errors.length || this.notices.length ) {
11046                 this.$body.after( this.$messages );
11047         } else {
11048                 this.$messages.remove();
11049                 return;
11050         }
11051
11052         for ( i = 0; i < this.notices.length; i++ ) {
11053                 this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
11054         }
11055         for ( i = 0; i < this.errors.length; i++ ) {
11056                 this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
11057         }
11058 };
11059
11060 /**
11061  * Include information about the widget's accessKey in our title. TitledElement calls this method.
11062  * (This is a bit of a hack.)
11063  *
11064  * @protected
11065  * @param {string} title Tooltip label for 'title' attribute
11066  * @return {string}
11067  */
11068 OO.ui.FieldLayout.prototype.formatTitleWithAccessKey = function ( title ) {
11069         if ( this.fieldWidget && this.fieldWidget.formatTitleWithAccessKey ) {
11070                 return this.fieldWidget.formatTitleWithAccessKey( title );
11071         }
11072         return title;
11073 };
11074
11075 /**
11076  * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11077  * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11078  * is required and is specified before any optional configuration settings.
11079  *
11080  * Labels can be aligned in one of four ways:
11081  *
11082  * - **left**: The label is placed before the field-widget and aligned with the left margin.
11083  *   A left-alignment is used for forms with many fields.
11084  * - **right**: The label is placed before the field-widget and aligned to the right margin.
11085  *   A right-alignment is used for long but familiar forms which users tab through,
11086  *   verifying the current field with a quick glance at the label.
11087  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11088  *   that users fill out from top to bottom.
11089  * - **inline**: The label is placed after the field-widget and aligned to the left.
11090  *   An inline-alignment is best used with checkboxes or radio buttons.
11091  *
11092  * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
11093  * text is specified.
11094  *
11095  *     @example
11096  *     // Example of an ActionFieldLayout
11097  *     var actionFieldLayout = new OO.ui.ActionFieldLayout(
11098  *         new OO.ui.TextInputWidget( {
11099  *             placeholder: 'Field widget'
11100  *         } ),
11101  *         new OO.ui.ButtonWidget( {
11102  *             label: 'Button'
11103  *         } ),
11104  *         {
11105  *             label: 'An ActionFieldLayout. This label is aligned top',
11106  *             align: 'top',
11107  *             help: 'This is help text'
11108  *         }
11109  *     );
11110  *
11111  *     $( 'body' ).append( actionFieldLayout.$element );
11112  *
11113  * @class
11114  * @extends OO.ui.FieldLayout
11115  *
11116  * @constructor
11117  * @param {OO.ui.Widget} fieldWidget Field widget
11118  * @param {OO.ui.ButtonWidget} buttonWidget Button widget
11119  * @param {Object} config
11120  */
11121 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
11122         // Allow passing positional parameters inside the config object
11123         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
11124                 config = fieldWidget;
11125                 fieldWidget = config.fieldWidget;
11126                 buttonWidget = config.buttonWidget;
11127         }
11128
11129         // Parent constructor
11130         OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
11131
11132         // Properties
11133         this.buttonWidget = buttonWidget;
11134         this.$button = $( '<span>' );
11135         this.$input = this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11136
11137         // Initialization
11138         this.$element
11139                 .addClass( 'oo-ui-actionFieldLayout' );
11140         this.$button
11141                 .addClass( 'oo-ui-actionFieldLayout-button' )
11142                 .append( this.buttonWidget.$element );
11143         this.$input
11144                 .addClass( 'oo-ui-actionFieldLayout-input' )
11145                 .append( this.fieldWidget.$element );
11146         this.$field
11147                 .append( this.$input, this.$button );
11148 };
11149
11150 /* Setup */
11151
11152 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
11153
11154 /**
11155  * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
11156  * which each contain an individual widget and, optionally, a label. Each Fieldset can be
11157  * configured with a label as well. For more information and examples,
11158  * please see the [OOjs UI documentation on MediaWiki][1].
11159  *
11160  *     @example
11161  *     // Example of a fieldset layout
11162  *     var input1 = new OO.ui.TextInputWidget( {
11163  *         placeholder: 'A text input field'
11164  *     } );
11165  *
11166  *     var input2 = new OO.ui.TextInputWidget( {
11167  *         placeholder: 'A text input field'
11168  *     } );
11169  *
11170  *     var fieldset = new OO.ui.FieldsetLayout( {
11171  *         label: 'Example of a fieldset layout'
11172  *     } );
11173  *
11174  *     fieldset.addItems( [
11175  *         new OO.ui.FieldLayout( input1, {
11176  *             label: 'Field One'
11177  *         } ),
11178  *         new OO.ui.FieldLayout( input2, {
11179  *             label: 'Field Two'
11180  *         } )
11181  *     ] );
11182  *     $( 'body' ).append( fieldset.$element );
11183  *
11184  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
11185  *
11186  * @class
11187  * @extends OO.ui.Layout
11188  * @mixins OO.ui.mixin.IconElement
11189  * @mixins OO.ui.mixin.LabelElement
11190  * @mixins OO.ui.mixin.GroupElement
11191  *
11192  * @constructor
11193  * @param {Object} [config] Configuration options
11194  * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
11195  * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
11196  *  in the upper-right corner of the rendered field; clicking it will display the text in a popup.
11197  *  For important messages, you are advised to use `notices`, as they are always shown.
11198  * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
11199  *  See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
11200  */
11201 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
11202         // Configuration initialization
11203         config = config || {};
11204
11205         // Parent constructor
11206         OO.ui.FieldsetLayout.parent.call( this, config );
11207
11208         // Mixin constructors
11209         OO.ui.mixin.IconElement.call( this, config );
11210         OO.ui.mixin.LabelElement.call( this, config );
11211         OO.ui.mixin.GroupElement.call( this, config );
11212
11213         // Properties
11214         this.$header = $( '<legend>' );
11215         if ( config.help ) {
11216                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
11217                         $overlay: config.$overlay,
11218                         popup: {
11219                                 padded: true
11220                         },
11221                         classes: [ 'oo-ui-fieldsetLayout-help' ],
11222                         framed: false,
11223                         icon: 'info'
11224                 } );
11225                 if ( config.help instanceof OO.ui.HtmlSnippet ) {
11226                         this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
11227                 } else {
11228                         this.popupButtonWidget.getPopup().$body.text( config.help );
11229                 }
11230                 this.$help = this.popupButtonWidget.$element;
11231         } else {
11232                 this.$help = $( [] );
11233         }
11234
11235         // Initialization
11236         this.$header
11237                 .addClass( 'oo-ui-fieldsetLayout-header' )
11238                 .append( this.$icon, this.$label, this.$help );
11239         this.$group.addClass( 'oo-ui-fieldsetLayout-group' );
11240         this.$element
11241                 .addClass( 'oo-ui-fieldsetLayout' )
11242                 .prepend( this.$header, this.$group );
11243         if ( Array.isArray( config.items ) ) {
11244                 this.addItems( config.items );
11245         }
11246 };
11247
11248 /* Setup */
11249
11250 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
11251 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
11252 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
11253 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
11254
11255 /* Static Properties */
11256
11257 /**
11258  * @static
11259  * @inheritdoc
11260  */
11261 OO.ui.FieldsetLayout.static.tagName = 'fieldset';
11262
11263 /**
11264  * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
11265  * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
11266  * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
11267  * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
11268  *
11269  * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
11270  * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
11271  * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
11272  * some fancier controls. Some controls have both regular and InputWidget variants, for example
11273  * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
11274  * often have simplified APIs to match the capabilities of HTML forms.
11275  * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
11276  *
11277  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
11278  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11279  *
11280  *     @example
11281  *     // Example of a form layout that wraps a fieldset layout
11282  *     var input1 = new OO.ui.TextInputWidget( {
11283  *         placeholder: 'Username'
11284  *     } );
11285  *     var input2 = new OO.ui.TextInputWidget( {
11286  *         placeholder: 'Password',
11287  *         type: 'password'
11288  *     } );
11289  *     var submit = new OO.ui.ButtonInputWidget( {
11290  *         label: 'Submit'
11291  *     } );
11292  *
11293  *     var fieldset = new OO.ui.FieldsetLayout( {
11294  *         label: 'A form layout'
11295  *     } );
11296  *     fieldset.addItems( [
11297  *         new OO.ui.FieldLayout( input1, {
11298  *             label: 'Username',
11299  *             align: 'top'
11300  *         } ),
11301  *         new OO.ui.FieldLayout( input2, {
11302  *             label: 'Password',
11303  *             align: 'top'
11304  *         } ),
11305  *         new OO.ui.FieldLayout( submit )
11306  *     ] );
11307  *     var form = new OO.ui.FormLayout( {
11308  *         items: [ fieldset ],
11309  *         action: '/api/formhandler',
11310  *         method: 'get'
11311  *     } )
11312  *     $( 'body' ).append( form.$element );
11313  *
11314  * @class
11315  * @extends OO.ui.Layout
11316  * @mixins OO.ui.mixin.GroupElement
11317  *
11318  * @constructor
11319  * @param {Object} [config] Configuration options
11320  * @cfg {string} [method] HTML form `method` attribute
11321  * @cfg {string} [action] HTML form `action` attribute
11322  * @cfg {string} [enctype] HTML form `enctype` attribute
11323  * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
11324  */
11325 OO.ui.FormLayout = function OoUiFormLayout( config ) {
11326         var action;
11327
11328         // Configuration initialization
11329         config = config || {};
11330
11331         // Parent constructor
11332         OO.ui.FormLayout.parent.call( this, config );
11333
11334         // Mixin constructors
11335         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11336
11337         // Events
11338         this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
11339
11340         // Make sure the action is safe
11341         action = config.action;
11342         if ( action !== undefined && !OO.ui.isSafeUrl( action ) ) {
11343                 action = './' + action;
11344         }
11345
11346         // Initialization
11347         this.$element
11348                 .addClass( 'oo-ui-formLayout' )
11349                 .attr( {
11350                         method: config.method,
11351                         action: action,
11352                         enctype: config.enctype
11353                 } );
11354         if ( Array.isArray( config.items ) ) {
11355                 this.addItems( config.items );
11356         }
11357 };
11358
11359 /* Setup */
11360
11361 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
11362 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
11363
11364 /* Events */
11365
11366 /**
11367  * A 'submit' event is emitted when the form is submitted.
11368  *
11369  * @event submit
11370  */
11371
11372 /* Static Properties */
11373
11374 /**
11375  * @static
11376  * @inheritdoc
11377  */
11378 OO.ui.FormLayout.static.tagName = 'form';
11379
11380 /* Methods */
11381
11382 /**
11383  * Handle form submit events.
11384  *
11385  * @private
11386  * @param {jQuery.Event} e Submit event
11387  * @fires submit
11388  */
11389 OO.ui.FormLayout.prototype.onFormSubmit = function () {
11390         if ( this.emit( 'submit' ) ) {
11391                 return false;
11392         }
11393 };
11394
11395 /**
11396  * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
11397  * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
11398  *
11399  *     @example
11400  *     // Example of a panel layout
11401  *     var panel = new OO.ui.PanelLayout( {
11402  *         expanded: false,
11403  *         framed: true,
11404  *         padded: true,
11405  *         $content: $( '<p>A panel layout with padding and a frame.</p>' )
11406  *     } );
11407  *     $( 'body' ).append( panel.$element );
11408  *
11409  * @class
11410  * @extends OO.ui.Layout
11411  *
11412  * @constructor
11413  * @param {Object} [config] Configuration options
11414  * @cfg {boolean} [scrollable=false] Allow vertical scrolling
11415  * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
11416  * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
11417  * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
11418  */
11419 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
11420         // Configuration initialization
11421         config = $.extend( {
11422                 scrollable: false,
11423                 padded: false,
11424                 expanded: true,
11425                 framed: false
11426         }, config );
11427
11428         // Parent constructor
11429         OO.ui.PanelLayout.parent.call( this, config );
11430
11431         // Initialization
11432         this.$element.addClass( 'oo-ui-panelLayout' );
11433         if ( config.scrollable ) {
11434                 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
11435         }
11436         if ( config.padded ) {
11437                 this.$element.addClass( 'oo-ui-panelLayout-padded' );
11438         }
11439         if ( config.expanded ) {
11440                 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
11441         }
11442         if ( config.framed ) {
11443                 this.$element.addClass( 'oo-ui-panelLayout-framed' );
11444         }
11445 };
11446
11447 /* Setup */
11448
11449 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
11450
11451 /* Methods */
11452
11453 /**
11454  * Focus the panel layout
11455  *
11456  * The default implementation just focuses the first focusable element in the panel
11457  */
11458 OO.ui.PanelLayout.prototype.focus = function () {
11459         OO.ui.findFocusable( this.$element ).focus();
11460 };
11461
11462 /**
11463  * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
11464  * items), with small margins between them. Convenient when you need to put a number of block-level
11465  * widgets on a single line next to each other.
11466  *
11467  * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
11468  *
11469  *     @example
11470  *     // HorizontalLayout with a text input and a label
11471  *     var layout = new OO.ui.HorizontalLayout( {
11472  *       items: [
11473  *         new OO.ui.LabelWidget( { label: 'Label' } ),
11474  *         new OO.ui.TextInputWidget( { value: 'Text' } )
11475  *       ]
11476  *     } );
11477  *     $( 'body' ).append( layout.$element );
11478  *
11479  * @class
11480  * @extends OO.ui.Layout
11481  * @mixins OO.ui.mixin.GroupElement
11482  *
11483  * @constructor
11484  * @param {Object} [config] Configuration options
11485  * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
11486  */
11487 OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
11488         // Configuration initialization
11489         config = config || {};
11490
11491         // Parent constructor
11492         OO.ui.HorizontalLayout.parent.call( this, config );
11493
11494         // Mixin constructors
11495         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11496
11497         // Initialization
11498         this.$element.addClass( 'oo-ui-horizontalLayout' );
11499         if ( Array.isArray( config.items ) ) {
11500                 this.addItems( config.items );
11501         }
11502 };
11503
11504 /* Setup */
11505
11506 OO.inheritClass( OO.ui.HorizontalLayout, OO.ui.Layout );
11507 OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
11508
11509 }( OO ) );
11510
11511 //# sourceMappingURL=oojs-ui-core.js.map