Make AuthManager::getAuthenticationProvider() public
[mediawiki.git] / resources / lib / oojs-ui / oojs-ui-core.js
blob6836553eeef36cc928fe3381b5a746ca8a02cf4e
1 /*!
2  * OOjs UI v0.17.4
3  * https://www.mediawiki.org/wiki/OOjs_UI
4  *
5  * Copyright 2011–2016 OOjs UI Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2016-05-31T21:50:52Z
10  */
11 ( function ( OO ) {
13 'use strict';
15 /**
16  * Namespace for all classes, static methods and static properties.
17  *
18  * @class
19  * @singleton
20  */
21 OO.ui = {};
23 OO.ui.bind = $.proxy;
25 /**
26  * @property {Object}
27  */
28 OO.ui.Keys = {
29         UNDEFINED: 0,
30         BACKSPACE: 8,
31         DELETE: 46,
32         LEFT: 37,
33         RIGHT: 39,
34         UP: 38,
35         DOWN: 40,
36         ENTER: 13,
37         END: 35,
38         HOME: 36,
39         TAB: 9,
40         PAGEUP: 33,
41         PAGEDOWN: 34,
42         ESCAPE: 27,
43         SHIFT: 16,
44         SPACE: 32
47 /**
48  * Constants for MouseEvent.which
49  *
50  * @property {Object}
51  */
52 OO.ui.MouseButtons = {
53         LEFT: 1,
54         MIDDLE: 2,
55         RIGHT: 3
58 /**
59  * @property {number}
60  */
61 OO.ui.elementId = 0;
63 /**
64  * Generate a unique ID for element
65  *
66  * @return {string} [id]
67  */
68 OO.ui.generateElementId = function () {
69         OO.ui.elementId += 1;
70         return 'oojsui-' + OO.ui.elementId;
73 /**
74  * Check if an element is focusable.
75  * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
76  *
77  * @param {jQuery} $element Element to test
78  * @return {boolean}
79  */
80 OO.ui.isFocusableElement = function ( $element ) {
81         var nodeName,
82                 element = $element[ 0 ];
84         // Anything disabled is not focusable
85         if ( element.disabled ) {
86                 return false;
87         }
89         // Check if the element is visible
90         if ( !(
91                 // This is quicker than calling $element.is( ':visible' )
92                 $.expr.filters.visible( element ) &&
93                 // Check that all parents are visible
94                 !$element.parents().addBack().filter( function () {
95                         return $.css( this, 'visibility' ) === 'hidden';
96                 } ).length
97         ) ) {
98                 return false;
99         }
101         // Check if the element is ContentEditable, which is the string 'true'
102         if ( element.contentEditable === 'true' ) {
103                 return true;
104         }
106         // Anything with a non-negative numeric tabIndex is focusable.
107         // Use .prop to avoid browser bugs
108         if ( $element.prop( 'tabIndex' ) >= 0 ) {
109                 return true;
110         }
112         // Some element types are naturally focusable
113         // (indexOf is much faster than regex in Chrome and about the
114         // same in FF: https://jsperf.com/regex-vs-indexof-array2)
115         nodeName = element.nodeName.toLowerCase();
116         if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName ) !== -1 ) {
117                 return true;
118         }
120         // Links and areas are focusable if they have an href
121         if ( ( nodeName === 'a' || nodeName === 'area' ) && $element.attr( 'href' ) !== undefined ) {
122                 return true;
123         }
125         return false;
129  * Find a focusable child
131  * @param {jQuery} $container Container to search in
132  * @param {boolean} [backwards] Search backwards
133  * @return {jQuery} Focusable child, an empty jQuery object if none found
134  */
135 OO.ui.findFocusable = function ( $container, backwards ) {
136         var $focusable = $( [] ),
137                 // $focusableCandidates is a superset of things that
138                 // could get matched by isFocusableElement
139                 $focusableCandidates = $container
140                         .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
142         if ( backwards ) {
143                 $focusableCandidates = Array.prototype.reverse.call( $focusableCandidates );
144         }
146         $focusableCandidates.each( function () {
147                 var $this = $( this );
148                 if ( OO.ui.isFocusableElement( $this ) ) {
149                         $focusable = $this;
150                         return false;
151                 }
152         } );
153         return $focusable;
157  * Get the user's language and any fallback languages.
159  * These language codes are used to localize user interface elements in the user's language.
161  * In environments that provide a localization system, this function should be overridden to
162  * return the user's language(s). The default implementation returns English (en) only.
164  * @return {string[]} Language codes, in descending order of priority
165  */
166 OO.ui.getUserLanguages = function () {
167         return [ 'en' ];
171  * Get a value in an object keyed by language code.
173  * @param {Object.<string,Mixed>} obj Object keyed by language code
174  * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
175  * @param {string} [fallback] Fallback code, used if no matching language can be found
176  * @return {Mixed} Local value
177  */
178 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
179         var i, len, langs;
181         // Requested language
182         if ( obj[ lang ] ) {
183                 return obj[ lang ];
184         }
185         // Known user language
186         langs = OO.ui.getUserLanguages();
187         for ( i = 0, len = langs.length; i < len; i++ ) {
188                 lang = langs[ i ];
189                 if ( obj[ lang ] ) {
190                         return obj[ lang ];
191                 }
192         }
193         // Fallback language
194         if ( obj[ fallback ] ) {
195                 return obj[ fallback ];
196         }
197         // First existing language
198         for ( lang in obj ) {
199                 return obj[ lang ];
200         }
202         return undefined;
206  * Check if a node is contained within another node
208  * Similar to jQuery#contains except a list of containers can be supplied
209  * and a boolean argument allows you to include the container in the match list
211  * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
212  * @param {HTMLElement} contained Node to find
213  * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
214  * @return {boolean} The node is in the list of target nodes
215  */
216 OO.ui.contains = function ( containers, contained, matchContainers ) {
217         var i;
218         if ( !Array.isArray( containers ) ) {
219                 containers = [ containers ];
220         }
221         for ( i = containers.length - 1; i >= 0; i-- ) {
222                 if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
223                         return true;
224                 }
225         }
226         return false;
230  * Return a function, that, as long as it continues to be invoked, will not
231  * be triggered. The function will be called after it stops being called for
232  * N milliseconds. If `immediate` is passed, trigger the function on the
233  * leading edge, instead of the trailing.
235  * Ported from: http://underscorejs.org/underscore.js
237  * @param {Function} func
238  * @param {number} wait
239  * @param {boolean} immediate
240  * @return {Function}
241  */
242 OO.ui.debounce = function ( func, wait, immediate ) {
243         var timeout;
244         return function () {
245                 var context = this,
246                         args = arguments,
247                         later = function () {
248                                 timeout = null;
249                                 if ( !immediate ) {
250                                         func.apply( context, args );
251                                 }
252                         };
253                 if ( immediate && !timeout ) {
254                         func.apply( context, args );
255                 }
256                 if ( !timeout || wait ) {
257                         clearTimeout( timeout );
258                         timeout = setTimeout( later, wait );
259                 }
260         };
264  * Returns a function, that, when invoked, will only be triggered at most once
265  * during a given window of time. If called again during that window, it will
266  * wait until the window ends and then trigger itself again.
268  * As it's not knowable to the caller whether the function will actually run
269  * when the wrapper is called, return values from the function are entirely
270  * discarded.
272  * @param {Function} func
273  * @param {number} wait
274  * @return {Function}
275  */
276 OO.ui.throttle = function ( func, wait ) {
277         var context, args, timeout,
278                 previous = 0,
279                 run = function () {
280                         timeout = null;
281                         previous = OO.ui.now();
282                         func.apply( context, args );
283                 };
284         return function () {
285                 // Check how long it's been since the last time the function was
286                 // called, and whether it's more or less than the requested throttle
287                 // period. If it's less, run the function immediately. If it's more,
288                 // set a timeout for the remaining time -- but don't replace an
289                 // existing timeout, since that'd indefinitely prolong the wait.
290                 var remaining = wait - ( OO.ui.now() - previous );
291                 context = this;
292                 args = arguments;
293                 if ( remaining <= 0 ) {
294                         // Note: unless wait was ridiculously large, this means we'll
295                         // automatically run the first time the function was called in a
296                         // given period. (If you provide a wait period larger than the
297                         // current Unix timestamp, you *deserve* unexpected behavior.)
298                         clearTimeout( timeout );
299                         run();
300                 } else if ( !timeout ) {
301                         timeout = setTimeout( run, remaining );
302                 }
303         };
307  * A (possibly faster) way to get the current timestamp as an integer
309  * @return {number} Current timestamp
310  */
311 OO.ui.now = Date.now || function () {
312         return new Date().getTime();
316  * Proxy for `node.addEventListener( eventName, handler, true )`.
318  * @param {HTMLElement} node
319  * @param {string} eventName
320  * @param {Function} handler
321  * @deprecated since 0.15.0
322  */
323 OO.ui.addCaptureEventListener = function ( node, eventName, handler ) {
324         node.addEventListener( eventName, handler, true );
328  * Proxy for `node.removeEventListener( eventName, handler, true )`.
330  * @param {HTMLElement} node
331  * @param {string} eventName
332  * @param {Function} handler
333  * @deprecated since 0.15.0
334  */
335 OO.ui.removeCaptureEventListener = function ( node, eventName, handler ) {
336         node.removeEventListener( eventName, handler, true );
340  * Reconstitute a JavaScript object corresponding to a widget created by
341  * the PHP implementation.
343  * This is an alias for `OO.ui.Element.static.infuse()`.
345  * @param {string|HTMLElement|jQuery} idOrNode
346  *   A DOM id (if a string) or node for the widget to infuse.
347  * @return {OO.ui.Element}
348  *   The `OO.ui.Element` corresponding to this (infusable) document node.
349  */
350 OO.ui.infuse = function ( idOrNode ) {
351         return OO.ui.Element.static.infuse( idOrNode );
354 ( function () {
355         /**
356          * Message store for the default implementation of OO.ui.msg
357          *
358          * Environments that provide a localization system should not use this, but should override
359          * OO.ui.msg altogether.
360          *
361          * @private
362          */
363         var messages = {
364                 // Tool tip for a button that moves items in a list down one place
365                 'ooui-outline-control-move-down': 'Move item down',
366                 // Tool tip for a button that moves items in a list up one place
367                 'ooui-outline-control-move-up': 'Move item up',
368                 // Tool tip for a button that removes items from a list
369                 'ooui-outline-control-remove': 'Remove item',
370                 // Label for the toolbar group that contains a list of all other available tools
371                 'ooui-toolbar-more': 'More',
372                 // Label for the fake tool that expands the full list of tools in a toolbar group
373                 'ooui-toolgroup-expand': 'More',
374                 // Label for the fake tool that collapses the full list of tools in a toolbar group
375                 'ooui-toolgroup-collapse': 'Fewer',
376                 // Default label for the accept button of a confirmation dialog
377                 'ooui-dialog-message-accept': 'OK',
378                 // Default label for the reject button of a confirmation dialog
379                 'ooui-dialog-message-reject': 'Cancel',
380                 // Title for process dialog error description
381                 'ooui-dialog-process-error': 'Something went wrong',
382                 // Label for process dialog dismiss error button, visible when describing errors
383                 'ooui-dialog-process-dismiss': 'Dismiss',
384                 // Label for process dialog retry action button, visible when describing only recoverable errors
385                 'ooui-dialog-process-retry': 'Try again',
386                 // Label for process dialog retry action button, visible when describing only warnings
387                 'ooui-dialog-process-continue': 'Continue',
388                 // Label for the file selection widget's select file button
389                 'ooui-selectfile-button-select': 'Select a file',
390                 // Label for the file selection widget if file selection is not supported
391                 'ooui-selectfile-not-supported': 'File selection is not supported',
392                 // Label for the file selection widget when no file is currently selected
393                 'ooui-selectfile-placeholder': 'No file is selected',
394                 // Label for the file selection widget's drop target
395                 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
396         };
398         /**
399          * Get a localized message.
400          *
401          * In environments that provide a localization system, this function should be overridden to
402          * return the message translated in the user's language. The default implementation always returns
403          * English messages.
404          *
405          * After the message key, message parameters may optionally be passed. In the default implementation,
406          * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
407          * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
408          * they support unnamed, ordered message parameters.
409          *
410          * @param {string} key Message key
411          * @param {...Mixed} [params] Message parameters
412          * @return {string} Translated message with parameters substituted
413          */
414         OO.ui.msg = function ( key ) {
415                 var message = messages[ key ],
416                         params = Array.prototype.slice.call( arguments, 1 );
417                 if ( typeof message === 'string' ) {
418                         // Perform $1 substitution
419                         message = message.replace( /\$(\d+)/g, function ( unused, n ) {
420                                 var i = parseInt( n, 10 );
421                                 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
422                         } );
423                 } else {
424                         // Return placeholder if message not found
425                         message = '[' + key + ']';
426                 }
427                 return message;
428         };
429 } )();
432  * Package a message and arguments for deferred resolution.
434  * Use this when you are statically specifying a message and the message may not yet be present.
436  * @param {string} key Message key
437  * @param {...Mixed} [params] Message parameters
438  * @return {Function} Function that returns the resolved message when executed
439  */
440 OO.ui.deferMsg = function () {
441         var args = arguments;
442         return function () {
443                 return OO.ui.msg.apply( OO.ui, args );
444         };
448  * Resolve a message.
450  * If the message is a function it will be executed, otherwise it will pass through directly.
452  * @param {Function|string} msg Deferred message, or message text
453  * @return {string} Resolved message
454  */
455 OO.ui.resolveMsg = function ( msg ) {
456         if ( $.isFunction( msg ) ) {
457                 return msg();
458         }
459         return msg;
463  * @param {string} url
464  * @return {boolean}
465  */
466 OO.ui.isSafeUrl = function ( url ) {
467         // Keep this function in sync with php/Tag.php
468         var i, protocolWhitelist;
470         function stringStartsWith( haystack, needle ) {
471                 return haystack.substr( 0, needle.length ) === needle;
472         }
474         protocolWhitelist = [
475                 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
476                 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
477                 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
478         ];
480         if ( url === '' ) {
481                 return true;
482         }
484         for ( i = 0; i < protocolWhitelist.length; i++ ) {
485                 if ( stringStartsWith( url, protocolWhitelist[ i ] + ':' ) ) {
486                         return true;
487                 }
488         }
490         // This matches '//' too
491         if ( stringStartsWith( url, '/' ) || stringStartsWith( url, './' ) ) {
492                 return true;
493         }
494         if ( stringStartsWith( url, '?' ) || stringStartsWith( url, '#' ) ) {
495                 return true;
496         }
498         return false;
502  * Mixin namespace.
503  */
506  * Namespace for OOjs UI mixins.
508  * Mixins are named according to the type of object they are intended to
509  * be mixed in to.  For example, OO.ui.mixin.GroupElement is intended to be
510  * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
511  * is intended to be mixed in to an instance of OO.ui.Widget.
513  * @class
514  * @singleton
515  */
516 OO.ui.mixin = {};
519  * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
520  * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
521  * connected to them and can't be interacted with.
523  * @abstract
524  * @class
526  * @constructor
527  * @param {Object} [config] Configuration options
528  * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
529  *  to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
530  *  for an example.
531  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
532  * @cfg {string} [id] The HTML id attribute used in the rendered tag.
533  * @cfg {string} [text] Text to insert
534  * @cfg {Array} [content] An array of content elements to append (after #text).
535  *  Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
536  *  Instances of OO.ui.Element will have their $element appended.
537  * @cfg {jQuery} [$content] Content elements to append (after #text).
538  * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
539  * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
540  *  Data can also be specified with the #setData method.
541  */
542 OO.ui.Element = function OoUiElement( config ) {
543         // Configuration initialization
544         config = config || {};
546         // Properties
547         this.$ = $;
548         this.visible = true;
549         this.data = config.data;
550         this.$element = config.$element ||
551                 $( document.createElement( this.getTagName() ) );
552         this.elementGroup = null;
553         this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses );
555         // Initialization
556         if ( Array.isArray( config.classes ) ) {
557                 this.$element.addClass( config.classes.join( ' ' ) );
558         }
559         if ( config.id ) {
560                 this.$element.attr( 'id', config.id );
561         }
562         if ( config.text ) {
563                 this.$element.text( config.text );
564         }
565         if ( config.content ) {
566                 // The `content` property treats plain strings as text; use an
567                 // HtmlSnippet to append HTML content.  `OO.ui.Element`s get their
568                 // appropriate $element appended.
569                 this.$element.append( config.content.map( function ( v ) {
570                         if ( typeof v === 'string' ) {
571                                 // Escape string so it is properly represented in HTML.
572                                 return document.createTextNode( v );
573                         } else if ( v instanceof OO.ui.HtmlSnippet ) {
574                                 // Bypass escaping.
575                                 return v.toString();
576                         } else if ( v instanceof OO.ui.Element ) {
577                                 return v.$element;
578                         }
579                         return v;
580                 } ) );
581         }
582         if ( config.$content ) {
583                 // The `$content` property treats plain strings as HTML.
584                 this.$element.append( config.$content );
585         }
588 /* Setup */
590 OO.initClass( OO.ui.Element );
592 /* Static Properties */
595  * The name of the HTML tag used by the element.
597  * The static value may be ignored if the #getTagName method is overridden.
599  * @static
600  * @inheritable
601  * @property {string}
602  */
603 OO.ui.Element.static.tagName = 'div';
605 /* Static Methods */
608  * Reconstitute a JavaScript object corresponding to a widget created
609  * by the PHP implementation.
611  * @param {string|HTMLElement|jQuery} idOrNode
612  *   A DOM id (if a string) or node for the widget to infuse.
613  * @return {OO.ui.Element}
614  *   The `OO.ui.Element` corresponding to this (infusable) document node.
615  *   For `Tag` objects emitted on the HTML side (used occasionally for content)
616  *   the value returned is a newly-created Element wrapping around the existing
617  *   DOM node.
618  */
619 OO.ui.Element.static.infuse = function ( idOrNode ) {
620         var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
621         // Verify that the type matches up.
622         // FIXME: uncomment after T89721 is fixed (see T90929)
623         /*
624         if ( !( obj instanceof this['class'] ) ) {
625                 throw new Error( 'Infusion type mismatch!' );
626         }
627         */
628         return obj;
632  * Implementation helper for `infuse`; skips the type check and has an
633  * extra property so that only the top-level invocation touches the DOM.
635  * @private
636  * @param {string|HTMLElement|jQuery} idOrNode
637  * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
638  *     when the top-level widget of this infusion is inserted into DOM,
639  *     replacing the original node; or false for top-level invocation.
640  * @return {OO.ui.Element}
641  */
642 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
643         // look for a cached result of a previous infusion.
644         var id, $elem, data, cls, parts, parent, obj, top, state, infusedChildren;
645         if ( typeof idOrNode === 'string' ) {
646                 id = idOrNode;
647                 $elem = $( document.getElementById( id ) );
648         } else {
649                 $elem = $( idOrNode );
650                 id = $elem.attr( 'id' );
651         }
652         if ( !$elem.length ) {
653                 throw new Error( 'Widget not found: ' + id );
654         }
655         if ( $elem[ 0 ].oouiInfused ) {
656                 $elem = $elem[ 0 ].oouiInfused;
657         }
658         data = $elem.data( 'ooui-infused' );
659         if ( data ) {
660                 // cached!
661                 if ( data === true ) {
662                         throw new Error( 'Circular dependency! ' + id );
663                 }
664                 if ( domPromise ) {
665                         // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
666                         state = data.constructor.static.gatherPreInfuseState( $elem, data );
667                         // restore dynamic state after the new element is re-inserted into DOM under infused parent
668                         domPromise.done( data.restorePreInfuseState.bind( data, state ) );
669                         infusedChildren = $elem.data( 'ooui-infused-children' );
670                         if ( infusedChildren && infusedChildren.length ) {
671                                 infusedChildren.forEach( function ( data ) {
672                                         var state = data.constructor.static.gatherPreInfuseState( $elem, data );
673                                         domPromise.done( data.restorePreInfuseState.bind( data, state ) );
674                                 } );
675                         }
676                 }
677                 return data;
678         }
679         data = $elem.attr( 'data-ooui' );
680         if ( !data ) {
681                 throw new Error( 'No infusion data found: ' + id );
682         }
683         try {
684                 data = $.parseJSON( data );
685         } catch ( _ ) {
686                 data = null;
687         }
688         if ( !( data && data._ ) ) {
689                 throw new Error( 'No valid infusion data found: ' + id );
690         }
691         if ( data._ === 'Tag' ) {
692                 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
693                 return new OO.ui.Element( { $element: $elem } );
694         }
695         parts = data._.split( '.' );
696         cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
697         if ( cls === undefined ) {
698                 // The PHP output might be old and not including the "OO.ui" prefix
699                 // TODO: Remove this back-compat after next major release
700                 cls = OO.getProp.apply( OO, [ OO.ui ].concat( parts ) );
701                 if ( cls === undefined ) {
702                         throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
703                 }
704         }
706         // Verify that we're creating an OO.ui.Element instance
707         parent = cls.parent;
709         while ( parent !== undefined ) {
710                 if ( parent === OO.ui.Element ) {
711                         // Safe
712                         break;
713                 }
715                 parent = parent.parent;
716         }
718         if ( parent !== OO.ui.Element ) {
719                 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
720         }
722         if ( domPromise === false ) {
723                 top = $.Deferred();
724                 domPromise = top.promise();
725         }
726         $elem.data( 'ooui-infused', true ); // prevent loops
727         data.id = id; // implicit
728         infusedChildren = [];
729         data = OO.copy( data, null, function deserialize( value ) {
730                 var infused;
731                 if ( OO.isPlainObject( value ) ) {
732                         if ( value.tag ) {
733                                 infused = OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
734                                 infusedChildren.push( infused );
735                                 // Flatten the structure
736                                 infusedChildren.push.apply( infusedChildren, infused.$element.data( 'ooui-infused-children' ) || [] );
737                                 infused.$element.removeData( 'ooui-infused-children' );
738                                 return infused;
739                         }
740                         if ( value.html !== undefined ) {
741                                 return new OO.ui.HtmlSnippet( value.html );
742                         }
743                 }
744         } );
745         // allow widgets to reuse parts of the DOM
746         data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data );
747         // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
748         state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
749         // rebuild widget
750         // jscs:disable requireCapitalizedConstructors
751         obj = new cls( data );
752         // jscs:enable requireCapitalizedConstructors
753         // now replace old DOM with this new DOM.
754         if ( top ) {
755                 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
756                 // so only mutate the DOM if we need to.
757                 if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
758                         $elem.replaceWith( obj.$element );
759                         // This element is now gone from the DOM, but if anyone is holding a reference to it,
760                         // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
761                         // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
762                         $elem[ 0 ].oouiInfused = obj.$element;
763                 }
764                 top.resolve();
765         }
766         obj.$element.data( 'ooui-infused', obj );
767         obj.$element.data( 'ooui-infused-children', infusedChildren );
768         // set the 'data-ooui' attribute so we can identify infused widgets
769         obj.$element.attr( 'data-ooui', '' );
770         // restore dynamic state after the new element is inserted into DOM
771         domPromise.done( obj.restorePreInfuseState.bind( obj, state ) );
772         return obj;
776  * Pick out parts of `node`'s DOM to be reused when infusing a widget.
778  * This method **must not** make any changes to the DOM, only find interesting pieces and add them
779  * to `config` (which should then be returned). Actual DOM juggling should then be done by the
780  * constructor, which will be given the enhanced config.
782  * @protected
783  * @param {HTMLElement} node
784  * @param {Object} config
785  * @return {Object}
786  */
787 OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) {
788         return config;
792  * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
793  * (and its children) that represent an Element of the same class and the given configuration,
794  * generated by the PHP implementation.
796  * This method is called just before `node` is detached from the DOM. The return value of this
797  * function will be passed to #restorePreInfuseState after the newly created widget's #$element
798  * is inserted into DOM to replace `node`.
800  * @protected
801  * @param {HTMLElement} node
802  * @param {Object} config
803  * @return {Object}
804  */
805 OO.ui.Element.static.gatherPreInfuseState = function () {
806         return {};
810  * Get a jQuery function within a specific document.
812  * @static
813  * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
814  * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
815  *   not in an iframe
816  * @return {Function} Bound jQuery function
817  */
818 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
819         function wrapper( selector ) {
820                 return $( selector, wrapper.context );
821         }
823         wrapper.context = this.getDocument( context );
825         if ( $iframe ) {
826                 wrapper.$iframe = $iframe;
827         }
829         return wrapper;
833  * Get the document of an element.
835  * @static
836  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
837  * @return {HTMLDocument|null} Document object
838  */
839 OO.ui.Element.static.getDocument = function ( obj ) {
840         // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
841         return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
842                 // Empty jQuery selections might have a context
843                 obj.context ||
844                 // HTMLElement
845                 obj.ownerDocument ||
846                 // Window
847                 obj.document ||
848                 // HTMLDocument
849                 ( obj.nodeType === 9 && obj ) ||
850                 null;
854  * Get the window of an element or document.
856  * @static
857  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
858  * @return {Window} Window object
859  */
860 OO.ui.Element.static.getWindow = function ( obj ) {
861         var doc = this.getDocument( obj );
862         return doc.defaultView;
866  * Get the direction of an element or document.
868  * @static
869  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
870  * @return {string} Text direction, either 'ltr' or 'rtl'
871  */
872 OO.ui.Element.static.getDir = function ( obj ) {
873         var isDoc, isWin;
875         if ( obj instanceof jQuery ) {
876                 obj = obj[ 0 ];
877         }
878         isDoc = obj.nodeType === 9;
879         isWin = obj.document !== undefined;
880         if ( isDoc || isWin ) {
881                 if ( isWin ) {
882                         obj = obj.document;
883                 }
884                 obj = obj.body;
885         }
886         return $( obj ).css( 'direction' );
890  * Get the offset between two frames.
892  * TODO: Make this function not use recursion.
894  * @static
895  * @param {Window} from Window of the child frame
896  * @param {Window} [to=window] Window of the parent frame
897  * @param {Object} [offset] Offset to start with, used internally
898  * @return {Object} Offset object, containing left and top properties
899  */
900 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
901         var i, len, frames, frame, rect;
903         if ( !to ) {
904                 to = window;
905         }
906         if ( !offset ) {
907                 offset = { top: 0, left: 0 };
908         }
909         if ( from.parent === from ) {
910                 return offset;
911         }
913         // Get iframe element
914         frames = from.parent.document.getElementsByTagName( 'iframe' );
915         for ( i = 0, len = frames.length; i < len; i++ ) {
916                 if ( frames[ i ].contentWindow === from ) {
917                         frame = frames[ i ];
918                         break;
919                 }
920         }
922         // Recursively accumulate offset values
923         if ( frame ) {
924                 rect = frame.getBoundingClientRect();
925                 offset.left += rect.left;
926                 offset.top += rect.top;
927                 if ( from !== to ) {
928                         this.getFrameOffset( from.parent, offset );
929                 }
930         }
931         return offset;
935  * Get the offset between two elements.
937  * The two elements may be in a different frame, but in that case the frame $element is in must
938  * be contained in the frame $anchor is in.
940  * @static
941  * @param {jQuery} $element Element whose position to get
942  * @param {jQuery} $anchor Element to get $element's position relative to
943  * @return {Object} Translated position coordinates, containing top and left properties
944  */
945 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
946         var iframe, iframePos,
947                 pos = $element.offset(),
948                 anchorPos = $anchor.offset(),
949                 elementDocument = this.getDocument( $element ),
950                 anchorDocument = this.getDocument( $anchor );
952         // If $element isn't in the same document as $anchor, traverse up
953         while ( elementDocument !== anchorDocument ) {
954                 iframe = elementDocument.defaultView.frameElement;
955                 if ( !iframe ) {
956                         throw new Error( '$element frame is not contained in $anchor frame' );
957                 }
958                 iframePos = $( iframe ).offset();
959                 pos.left += iframePos.left;
960                 pos.top += iframePos.top;
961                 elementDocument = iframe.ownerDocument;
962         }
963         pos.left -= anchorPos.left;
964         pos.top -= anchorPos.top;
965         return pos;
969  * Get element border sizes.
971  * @static
972  * @param {HTMLElement} el Element to measure
973  * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
974  */
975 OO.ui.Element.static.getBorders = function ( el ) {
976         var doc = el.ownerDocument,
977                 win = doc.defaultView,
978                 style = win.getComputedStyle( el, null ),
979                 $el = $( el ),
980                 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
981                 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
982                 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
983                 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
985         return {
986                 top: top,
987                 left: left,
988                 bottom: bottom,
989                 right: right
990         };
994  * Get dimensions of an element or window.
996  * @static
997  * @param {HTMLElement|Window} el Element to measure
998  * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
999  */
1000 OO.ui.Element.static.getDimensions = function ( el ) {
1001         var $el, $win,
1002                 doc = el.ownerDocument || el.document,
1003                 win = doc.defaultView;
1005         if ( win === el || el === doc.documentElement ) {
1006                 $win = $( win );
1007                 return {
1008                         borders: { top: 0, left: 0, bottom: 0, right: 0 },
1009                         scroll: {
1010                                 top: $win.scrollTop(),
1011                                 left: $win.scrollLeft()
1012                         },
1013                         scrollbar: { right: 0, bottom: 0 },
1014                         rect: {
1015                                 top: 0,
1016                                 left: 0,
1017                                 bottom: $win.innerHeight(),
1018                                 right: $win.innerWidth()
1019                         }
1020                 };
1021         } else {
1022                 $el = $( el );
1023                 return {
1024                         borders: this.getBorders( el ),
1025                         scroll: {
1026                                 top: $el.scrollTop(),
1027                                 left: $el.scrollLeft()
1028                         },
1029                         scrollbar: {
1030                                 right: $el.innerWidth() - el.clientWidth,
1031                                 bottom: $el.innerHeight() - el.clientHeight
1032                         },
1033                         rect: el.getBoundingClientRect()
1034                 };
1035         }
1039  * Get scrollable object parent
1041  * documentElement can't be used to get or set the scrollTop
1042  * property on Blink. Changing and testing its value lets us
1043  * use 'body' or 'documentElement' based on what is working.
1045  * https://code.google.com/p/chromium/issues/detail?id=303131
1047  * @static
1048  * @param {HTMLElement} el Element to find scrollable parent for
1049  * @return {HTMLElement} Scrollable parent
1050  */
1051 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1052         var scrollTop, body;
1054         if ( OO.ui.scrollableElement === undefined ) {
1055                 body = el.ownerDocument.body;
1056                 scrollTop = body.scrollTop;
1057                 body.scrollTop = 1;
1059                 if ( body.scrollTop === 1 ) {
1060                         body.scrollTop = scrollTop;
1061                         OO.ui.scrollableElement = 'body';
1062                 } else {
1063                         OO.ui.scrollableElement = 'documentElement';
1064                 }
1065         }
1067         return el.ownerDocument[ OO.ui.scrollableElement ];
1071  * Get closest scrollable container.
1073  * Traverses up until either a scrollable element or the root is reached, in which case the window
1074  * will be returned.
1076  * @static
1077  * @param {HTMLElement} el Element to find scrollable container for
1078  * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1079  * @return {HTMLElement} Closest scrollable container
1080  */
1081 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1082         var i, val,
1083                 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1084                 props = [ 'overflow-x', 'overflow-y' ],
1085                 $parent = $( el ).parent();
1087         if ( dimension === 'x' || dimension === 'y' ) {
1088                 props = [ 'overflow-' + dimension ];
1089         }
1091         while ( $parent.length ) {
1092                 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1093                         return $parent[ 0 ];
1094                 }
1095                 i = props.length;
1096                 while ( i-- ) {
1097                         val = $parent.css( props[ i ] );
1098                         if ( val === 'auto' || val === 'scroll' ) {
1099                                 return $parent[ 0 ];
1100                         }
1101                 }
1102                 $parent = $parent.parent();
1103         }
1104         return this.getDocument( el ).body;
1108  * Scroll element into view.
1110  * @static
1111  * @param {HTMLElement} el Element to scroll into view
1112  * @param {Object} [config] Configuration options
1113  * @param {string} [config.duration='fast'] jQuery animation duration value
1114  * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1115  *  to scroll in both directions
1116  * @param {Function} [config.complete] Function to call when scrolling completes.
1117  *  Deprecated since 0.15.4, use the return promise instead.
1118  * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1119  */
1120 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1121         var position, animations, callback, container, $container, elementDimensions, containerDimensions, $window,
1122                 deferred = $.Deferred();
1124         // Configuration initialization
1125         config = config || {};
1127         animations = {};
1128         callback = typeof config.complete === 'function' && config.complete;
1129         container = this.getClosestScrollableContainer( el, config.direction );
1130         $container = $( container );
1131         elementDimensions = this.getDimensions( el );
1132         containerDimensions = this.getDimensions( container );
1133         $window = $( this.getWindow( el ) );
1135         // Compute the element's position relative to the container
1136         if ( $container.is( 'html, body' ) ) {
1137                 // If the scrollable container is the root, this is easy
1138                 position = {
1139                         top: elementDimensions.rect.top,
1140                         bottom: $window.innerHeight() - elementDimensions.rect.bottom,
1141                         left: elementDimensions.rect.left,
1142                         right: $window.innerWidth() - elementDimensions.rect.right
1143                 };
1144         } else {
1145                 // Otherwise, we have to subtract el's coordinates from container's coordinates
1146                 position = {
1147                         top: elementDimensions.rect.top - ( containerDimensions.rect.top + containerDimensions.borders.top ),
1148                         bottom: containerDimensions.rect.bottom - containerDimensions.borders.bottom - containerDimensions.scrollbar.bottom - elementDimensions.rect.bottom,
1149                         left: elementDimensions.rect.left - ( containerDimensions.rect.left + containerDimensions.borders.left ),
1150                         right: containerDimensions.rect.right - containerDimensions.borders.right - containerDimensions.scrollbar.right - elementDimensions.rect.right
1151                 };
1152         }
1154         if ( !config.direction || config.direction === 'y' ) {
1155                 if ( position.top < 0 ) {
1156                         animations.scrollTop = containerDimensions.scroll.top + position.top;
1157                 } else if ( position.top > 0 && position.bottom < 0 ) {
1158                         animations.scrollTop = containerDimensions.scroll.top + Math.min( position.top, -position.bottom );
1159                 }
1160         }
1161         if ( !config.direction || config.direction === 'x' ) {
1162                 if ( position.left < 0 ) {
1163                         animations.scrollLeft = containerDimensions.scroll.left + position.left;
1164                 } else if ( position.left > 0 && position.right < 0 ) {
1165                         animations.scrollLeft = containerDimensions.scroll.left + Math.min( position.left, -position.right );
1166                 }
1167         }
1168         if ( !$.isEmptyObject( animations ) ) {
1169                 $container.stop( true ).animate( animations, config.duration === undefined ? 'fast' : config.duration );
1170                 $container.queue( function ( next ) {
1171                         if ( callback ) {
1172                                 callback();
1173                         }
1174                         deferred.resolve();
1175                         next();
1176                 } );
1177         } else {
1178                 if ( callback ) {
1179                         callback();
1180                 }
1181                 deferred.resolve();
1182         }
1183         return deferred.promise();
1187  * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1188  * and reserve space for them, because it probably doesn't.
1190  * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1191  * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1192  * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1193  * and then reattach (or show) them back.
1195  * @static
1196  * @param {HTMLElement} el Element to reconsider the scrollbars on
1197  */
1198 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1199         var i, len, scrollLeft, scrollTop, nodes = [];
1200         // Save scroll position
1201         scrollLeft = el.scrollLeft;
1202         scrollTop = el.scrollTop;
1203         // Detach all children
1204         while ( el.firstChild ) {
1205                 nodes.push( el.firstChild );
1206                 el.removeChild( el.firstChild );
1207         }
1208         // Force reflow
1209         void el.offsetHeight;
1210         // Reattach all children
1211         for ( i = 0, len = nodes.length; i < len; i++ ) {
1212                 el.appendChild( nodes[ i ] );
1213         }
1214         // Restore scroll position (no-op if scrollbars disappeared)
1215         el.scrollLeft = scrollLeft;
1216         el.scrollTop = scrollTop;
1219 /* Methods */
1222  * Toggle visibility of an element.
1224  * @param {boolean} [show] Make element visible, omit to toggle visibility
1225  * @fires visible
1226  * @chainable
1227  */
1228 OO.ui.Element.prototype.toggle = function ( show ) {
1229         show = show === undefined ? !this.visible : !!show;
1231         if ( show !== this.isVisible() ) {
1232                 this.visible = show;
1233                 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1234                 this.emit( 'toggle', show );
1235         }
1237         return this;
1241  * Check if element is visible.
1243  * @return {boolean} element is visible
1244  */
1245 OO.ui.Element.prototype.isVisible = function () {
1246         return this.visible;
1250  * Get element data.
1252  * @return {Mixed} Element data
1253  */
1254 OO.ui.Element.prototype.getData = function () {
1255         return this.data;
1259  * Set element data.
1261  * @param {Mixed} data Element data
1262  * @chainable
1263  */
1264 OO.ui.Element.prototype.setData = function ( data ) {
1265         this.data = data;
1266         return this;
1270  * Check if element supports one or more methods.
1272  * @param {string|string[]} methods Method or list of methods to check
1273  * @return {boolean} All methods are supported
1274  */
1275 OO.ui.Element.prototype.supports = function ( methods ) {
1276         var i, len,
1277                 support = 0;
1279         methods = Array.isArray( methods ) ? methods : [ methods ];
1280         for ( i = 0, len = methods.length; i < len; i++ ) {
1281                 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1282                         support++;
1283                 }
1284         }
1286         return methods.length === support;
1290  * Update the theme-provided classes.
1292  * @localdoc This is called in element mixins and widget classes any time state changes.
1293  *   Updating is debounced, minimizing overhead of changing multiple attributes and
1294  *   guaranteeing that theme updates do not occur within an element's constructor
1295  */
1296 OO.ui.Element.prototype.updateThemeClasses = function () {
1297         this.debouncedUpdateThemeClassesHandler();
1301  * @private
1302  * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1303  *   make them synchronous.
1304  */
1305 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1306         OO.ui.theme.updateElementClasses( this );
1310  * Get the HTML tag name.
1312  * Override this method to base the result on instance information.
1314  * @return {string} HTML tag name
1315  */
1316 OO.ui.Element.prototype.getTagName = function () {
1317         return this.constructor.static.tagName;
1321  * Check if the element is attached to the DOM
1323  * @return {boolean} The element is attached to the DOM
1324  */
1325 OO.ui.Element.prototype.isElementAttached = function () {
1326         return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1330  * Get the DOM document.
1332  * @return {HTMLDocument} Document object
1333  */
1334 OO.ui.Element.prototype.getElementDocument = function () {
1335         // Don't cache this in other ways either because subclasses could can change this.$element
1336         return OO.ui.Element.static.getDocument( this.$element );
1340  * Get the DOM window.
1342  * @return {Window} Window object
1343  */
1344 OO.ui.Element.prototype.getElementWindow = function () {
1345         return OO.ui.Element.static.getWindow( this.$element );
1349  * Get closest scrollable container.
1351  * @return {HTMLElement} Closest scrollable container
1352  */
1353 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1354         return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1358  * Get group element is in.
1360  * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1361  */
1362 OO.ui.Element.prototype.getElementGroup = function () {
1363         return this.elementGroup;
1367  * Set group element is in.
1369  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1370  * @chainable
1371  */
1372 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1373         this.elementGroup = group;
1374         return this;
1378  * Scroll element into view.
1380  * @param {Object} [config] Configuration options
1381  * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1382  */
1383 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1384         return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1388  * Restore the pre-infusion dynamic state for this widget.
1390  * This method is called after #$element has been inserted into DOM. The parameter is the return
1391  * value of #gatherPreInfuseState.
1393  * @protected
1394  * @param {Object} state
1395  */
1396 OO.ui.Element.prototype.restorePreInfuseState = function () {
1400  * Wraps an HTML snippet for use with configuration values which default
1401  * to strings.  This bypasses the default html-escaping done to string
1402  * values.
1404  * @class
1406  * @constructor
1407  * @param {string} [content] HTML content
1408  */
1409 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
1410         // Properties
1411         this.content = content;
1414 /* Setup */
1416 OO.initClass( OO.ui.HtmlSnippet );
1418 /* Methods */
1421  * Render into HTML.
1423  * @return {string} Unchanged HTML snippet.
1424  */
1425 OO.ui.HtmlSnippet.prototype.toString = function () {
1426         return this.content;
1430  * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1431  * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1432  * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1433  * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1434  * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1436  * @abstract
1437  * @class
1438  * @extends OO.ui.Element
1439  * @mixins OO.EventEmitter
1441  * @constructor
1442  * @param {Object} [config] Configuration options
1443  */
1444 OO.ui.Layout = function OoUiLayout( config ) {
1445         // Configuration initialization
1446         config = config || {};
1448         // Parent constructor
1449         OO.ui.Layout.parent.call( this, config );
1451         // Mixin constructors
1452         OO.EventEmitter.call( this );
1454         // Initialization
1455         this.$element.addClass( 'oo-ui-layout' );
1458 /* Setup */
1460 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1461 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1464  * Widgets are compositions of one or more OOjs UI elements that users can both view
1465  * and interact with. All widgets can be configured and modified via a standard API,
1466  * and their state can change dynamically according to a model.
1468  * @abstract
1469  * @class
1470  * @extends OO.ui.Element
1471  * @mixins OO.EventEmitter
1473  * @constructor
1474  * @param {Object} [config] Configuration options
1475  * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1476  *  appearance reflects this state.
1477  */
1478 OO.ui.Widget = function OoUiWidget( config ) {
1479         // Initialize config
1480         config = $.extend( { disabled: false }, config );
1482         // Parent constructor
1483         OO.ui.Widget.parent.call( this, config );
1485         // Mixin constructors
1486         OO.EventEmitter.call( this );
1488         // Properties
1489         this.disabled = null;
1490         this.wasDisabled = null;
1492         // Initialization
1493         this.$element.addClass( 'oo-ui-widget' );
1494         this.setDisabled( !!config.disabled );
1497 /* Setup */
1499 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1500 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1502 /* Static Properties */
1505  * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1506  * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1507  * handling.
1509  * @static
1510  * @inheritable
1511  * @property {boolean}
1512  */
1513 OO.ui.Widget.static.supportsSimpleLabel = false;
1515 /* Events */
1518  * @event disable
1520  * A 'disable' event is emitted when the disabled state of the widget changes
1521  * (i.e. on disable **and** enable).
1523  * @param {boolean} disabled Widget is disabled
1524  */
1527  * @event toggle
1529  * A 'toggle' event is emitted when the visibility of the widget changes.
1531  * @param {boolean} visible Widget is visible
1532  */
1534 /* Methods */
1537  * Check if the widget is disabled.
1539  * @return {boolean} Widget is disabled
1540  */
1541 OO.ui.Widget.prototype.isDisabled = function () {
1542         return this.disabled;
1546  * Set the 'disabled' state of the widget.
1548  * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1550  * @param {boolean} disabled Disable widget
1551  * @chainable
1552  */
1553 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1554         var isDisabled;
1556         this.disabled = !!disabled;
1557         isDisabled = this.isDisabled();
1558         if ( isDisabled !== this.wasDisabled ) {
1559                 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1560                 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1561                 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1562                 this.emit( 'disable', isDisabled );
1563                 this.updateThemeClasses();
1564         }
1565         this.wasDisabled = isDisabled;
1567         return this;
1571  * Update the disabled state, in case of changes in parent widget.
1573  * @chainable
1574  */
1575 OO.ui.Widget.prototype.updateDisabled = function () {
1576         this.setDisabled( this.disabled );
1577         return this;
1581  * Theme logic.
1583  * @abstract
1584  * @class
1586  * @constructor
1587  * @param {Object} [config] Configuration options
1588  */
1589 OO.ui.Theme = function OoUiTheme( config ) {
1590         // Configuration initialization
1591         config = config || {};
1594 /* Setup */
1596 OO.initClass( OO.ui.Theme );
1598 /* Methods */
1601  * Get a list of classes to be applied to a widget.
1603  * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1604  * otherwise state transitions will not work properly.
1606  * @param {OO.ui.Element} element Element for which to get classes
1607  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1608  */
1609 OO.ui.Theme.prototype.getElementClasses = function () {
1610         return { on: [], off: [] };
1614  * Update CSS classes provided by the theme.
1616  * For elements with theme logic hooks, this should be called any time there's a state change.
1618  * @param {OO.ui.Element} element Element for which to update classes
1619  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1620  */
1621 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
1622         var $elements = $( [] ),
1623                 classes = this.getElementClasses( element );
1625         if ( element.$icon ) {
1626                 $elements = $elements.add( element.$icon );
1627         }
1628         if ( element.$indicator ) {
1629                 $elements = $elements.add( element.$indicator );
1630         }
1632         $elements
1633                 .removeClass( classes.off.join( ' ' ) )
1634                 .addClass( classes.on.join( ' ' ) );
1638  * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1639  * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1640  * order in which users will navigate through the focusable elements via the "tab" key.
1642  *     @example
1643  *     // TabIndexedElement is mixed into the ButtonWidget class
1644  *     // to provide a tabIndex property.
1645  *     var button1 = new OO.ui.ButtonWidget( {
1646  *         label: 'fourth',
1647  *         tabIndex: 4
1648  *     } );
1649  *     var button2 = new OO.ui.ButtonWidget( {
1650  *         label: 'second',
1651  *         tabIndex: 2
1652  *     } );
1653  *     var button3 = new OO.ui.ButtonWidget( {
1654  *         label: 'third',
1655  *         tabIndex: 3
1656  *     } );
1657  *     var button4 = new OO.ui.ButtonWidget( {
1658  *         label: 'first',
1659  *         tabIndex: 1
1660  *     } );
1661  *     $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1663  * @abstract
1664  * @class
1666  * @constructor
1667  * @param {Object} [config] Configuration options
1668  * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1669  *  the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1670  *  functionality will be applied to it instead.
1671  * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1672  *  order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1673  *  to remove the element from the tab-navigation flow.
1674  */
1675 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
1676         // Configuration initialization
1677         config = $.extend( { tabIndex: 0 }, config );
1679         // Properties
1680         this.$tabIndexed = null;
1681         this.tabIndex = null;
1683         // Events
1684         this.connect( this, { disable: 'onTabIndexedElementDisable' } );
1686         // Initialization
1687         this.setTabIndex( config.tabIndex );
1688         this.setTabIndexedElement( config.$tabIndexed || this.$element );
1691 /* Setup */
1693 OO.initClass( OO.ui.mixin.TabIndexedElement );
1695 /* Methods */
1698  * Set the element that should use the tabindex functionality.
1700  * This method is used to retarget a tabindex mixin so that its functionality applies
1701  * to the specified element. If an element is currently using the functionality, the mixin’s
1702  * effect on that element is removed before the new element is set up.
1704  * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1705  * @chainable
1706  */
1707 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
1708         var tabIndex = this.tabIndex;
1709         // Remove attributes from old $tabIndexed
1710         this.setTabIndex( null );
1711         // Force update of new $tabIndexed
1712         this.$tabIndexed = $tabIndexed;
1713         this.tabIndex = tabIndex;
1714         return this.updateTabIndex();
1718  * Set the value of the tabindex.
1720  * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
1721  * @chainable
1722  */
1723 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
1724         tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
1726         if ( this.tabIndex !== tabIndex ) {
1727                 this.tabIndex = tabIndex;
1728                 this.updateTabIndex();
1729         }
1731         return this;
1735  * Update the `tabindex` attribute, in case of changes to tab index or
1736  * disabled state.
1738  * @private
1739  * @chainable
1740  */
1741 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
1742         if ( this.$tabIndexed ) {
1743                 if ( this.tabIndex !== null ) {
1744                         // Do not index over disabled elements
1745                         this.$tabIndexed.attr( {
1746                                 tabindex: this.isDisabled() ? -1 : this.tabIndex,
1747                                 // Support: ChromeVox and NVDA
1748                                 // These do not seem to inherit aria-disabled from parent elements
1749                                 'aria-disabled': this.isDisabled().toString()
1750                         } );
1751                 } else {
1752                         this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
1753                 }
1754         }
1755         return this;
1759  * Handle disable events.
1761  * @private
1762  * @param {boolean} disabled Element is disabled
1763  */
1764 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
1765         this.updateTabIndex();
1769  * Get the value of the tabindex.
1771  * @return {number|null} Tabindex value
1772  */
1773 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
1774         return this.tabIndex;
1778  * ButtonElement is often mixed into other classes to generate a button, which is a clickable
1779  * interface element that can be configured with access keys for accessibility.
1780  * See the [OOjs UI documentation on MediaWiki] [1] for examples.
1782  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
1784  * @abstract
1785  * @class
1787  * @constructor
1788  * @param {Object} [config] Configuration options
1789  * @cfg {jQuery} [$button] The button element created by the class.
1790  *  If this configuration is omitted, the button element will use a generated `<a>`.
1791  * @cfg {boolean} [framed=true] Render the button with a frame
1792  */
1793 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
1794         // Configuration initialization
1795         config = config || {};
1797         // Properties
1798         this.$button = null;
1799         this.framed = null;
1800         this.active = false;
1801         this.onMouseUpHandler = this.onMouseUp.bind( this );
1802         this.onMouseDownHandler = this.onMouseDown.bind( this );
1803         this.onKeyDownHandler = this.onKeyDown.bind( this );
1804         this.onKeyUpHandler = this.onKeyUp.bind( this );
1805         this.onClickHandler = this.onClick.bind( this );
1806         this.onKeyPressHandler = this.onKeyPress.bind( this );
1808         // Initialization
1809         this.$element.addClass( 'oo-ui-buttonElement' );
1810         this.toggleFramed( config.framed === undefined || config.framed );
1811         this.setButtonElement( config.$button || $( '<a>' ) );
1814 /* Setup */
1816 OO.initClass( OO.ui.mixin.ButtonElement );
1818 /* Static Properties */
1821  * Cancel mouse down events.
1823  * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
1824  * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
1825  * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
1826  * parent widget.
1828  * @static
1829  * @inheritable
1830  * @property {boolean}
1831  */
1832 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
1834 /* Events */
1837  * A 'click' event is emitted when the button element is clicked.
1839  * @event click
1840  */
1842 /* Methods */
1845  * Set the button element.
1847  * This method is used to retarget a button mixin so that its functionality applies to
1848  * the specified button element instead of the one created by the class. If a button element
1849  * is already set, the method will remove the mixin’s effect on that element.
1851  * @param {jQuery} $button Element to use as button
1852  */
1853 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
1854         if ( this.$button ) {
1855                 this.$button
1856                         .removeClass( 'oo-ui-buttonElement-button' )
1857                         .removeAttr( 'role accesskey' )
1858                         .off( {
1859                                 mousedown: this.onMouseDownHandler,
1860                                 keydown: this.onKeyDownHandler,
1861                                 click: this.onClickHandler,
1862                                 keypress: this.onKeyPressHandler
1863                         } );
1864         }
1866         this.$button = $button
1867                 .addClass( 'oo-ui-buttonElement-button' )
1868                 .attr( { role: 'button' } )
1869                 .on( {
1870                         mousedown: this.onMouseDownHandler,
1871                         keydown: this.onKeyDownHandler,
1872                         click: this.onClickHandler,
1873                         keypress: this.onKeyPressHandler
1874                 } );
1878  * Handles mouse down events.
1880  * @protected
1881  * @param {jQuery.Event} e Mouse down event
1882  */
1883 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
1884         if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
1885                 return;
1886         }
1887         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
1888         // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
1889         // reliably remove the pressed class
1890         this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
1891         // Prevent change of focus unless specifically configured otherwise
1892         if ( this.constructor.static.cancelButtonMouseDownEvents ) {
1893                 return false;
1894         }
1898  * Handles mouse up events.
1900  * @protected
1901  * @param {MouseEvent} e Mouse up event
1902  */
1903 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
1904         if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
1905                 return;
1906         }
1907         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
1908         // Stop listening for mouseup, since we only needed this once
1909         this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
1913  * Handles mouse click events.
1915  * @protected
1916  * @param {jQuery.Event} e Mouse click event
1917  * @fires click
1918  */
1919 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
1920         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
1921                 if ( this.emit( 'click' ) ) {
1922                         return false;
1923                 }
1924         }
1928  * Handles key down events.
1930  * @protected
1931  * @param {jQuery.Event} e Key down event
1932  */
1933 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
1934         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
1935                 return;
1936         }
1937         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
1938         // Run the keyup handler no matter where the key is when the button is let go, so we can
1939         // reliably remove the pressed class
1940         this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
1944  * Handles key up events.
1946  * @protected
1947  * @param {KeyboardEvent} e Key up event
1948  */
1949 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
1950         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
1951                 return;
1952         }
1953         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
1954         // Stop listening for keyup, since we only needed this once
1955         this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
1959  * Handles key press events.
1961  * @protected
1962  * @param {jQuery.Event} e Key press event
1963  * @fires click
1964  */
1965 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
1966         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
1967                 if ( this.emit( 'click' ) ) {
1968                         return false;
1969                 }
1970         }
1974  * Check if button has a frame.
1976  * @return {boolean} Button is framed
1977  */
1978 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
1979         return this.framed;
1983  * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
1985  * @param {boolean} [framed] Make button framed, omit to toggle
1986  * @chainable
1987  */
1988 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
1989         framed = framed === undefined ? !this.framed : !!framed;
1990         if ( framed !== this.framed ) {
1991                 this.framed = framed;
1992                 this.$element
1993                         .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
1994                         .toggleClass( 'oo-ui-buttonElement-framed', framed );
1995                 this.updateThemeClasses();
1996         }
1998         return this;
2002  * Set the button's active state.
2004  * The active state can be set on:
2006  *  - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2007  *  - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2008  *  - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2010  * @protected
2011  * @param {boolean} value Make button active
2012  * @chainable
2013  */
2014 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
2015         this.active = !!value;
2016         this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
2017         return this;
2021  * Check if the button is active
2023  * @protected
2024  * @return {boolean} The button is active
2025  */
2026 OO.ui.mixin.ButtonElement.prototype.isActive = function () {
2027         return this.active;
2031  * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2032  * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2033  * items from the group is done through the interface the class provides.
2034  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2036  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
2038  * @abstract
2039  * @class
2041  * @constructor
2042  * @param {Object} [config] Configuration options
2043  * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2044  *  is omitted, the group element will use a generated `<div>`.
2045  */
2046 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
2047         // Configuration initialization
2048         config = config || {};
2050         // Properties
2051         this.$group = null;
2052         this.items = [];
2053         this.aggregateItemEvents = {};
2055         // Initialization
2056         this.setGroupElement( config.$group || $( '<div>' ) );
2059 /* Events */
2062  * @event change
2064  * A change event is emitted when the set of selected items changes.
2066  * @param {OO.ui.Element[]} items Items currently in the group
2067  */
2069 /* Methods */
2072  * Set the group element.
2074  * If an element is already set, items will be moved to the new element.
2076  * @param {jQuery} $group Element to use as group
2077  */
2078 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
2079         var i, len;
2081         this.$group = $group;
2082         for ( i = 0, len = this.items.length; i < len; i++ ) {
2083                 this.$group.append( this.items[ i ].$element );
2084         }
2088  * Check if a group contains no items.
2090  * @return {boolean} Group is empty
2091  */
2092 OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
2093         return !this.items.length;
2097  * Get all items in the group.
2099  * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
2100  * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
2101  * from a group).
2103  * @return {OO.ui.Element[]} An array of items.
2104  */
2105 OO.ui.mixin.GroupElement.prototype.getItems = function () {
2106         return this.items.slice( 0 );
2110  * Get an item by its data.
2112  * Only the first item with matching data will be returned. To return all matching items,
2113  * use the #getItemsFromData method.
2115  * @param {Object} data Item data to search for
2116  * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2117  */
2118 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
2119         var i, len, item,
2120                 hash = OO.getHash( data );
2122         for ( i = 0, len = this.items.length; i < len; i++ ) {
2123                 item = this.items[ i ];
2124                 if ( hash === OO.getHash( item.getData() ) ) {
2125                         return item;
2126                 }
2127         }
2129         return null;
2133  * Get items by their data.
2135  * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2137  * @param {Object} data Item data to search for
2138  * @return {OO.ui.Element[]} Items with equivalent data
2139  */
2140 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
2141         var i, len, item,
2142                 hash = OO.getHash( data ),
2143                 items = [];
2145         for ( i = 0, len = this.items.length; i < len; i++ ) {
2146                 item = this.items[ i ];
2147                 if ( hash === OO.getHash( item.getData() ) ) {
2148                         items.push( item );
2149                 }
2150         }
2152         return items;
2156  * Aggregate the events emitted by the group.
2158  * When events are aggregated, the group will listen to all contained items for the event,
2159  * and then emit the event under a new name. The new event will contain an additional leading
2160  * parameter containing the item that emitted the original event. Other arguments emitted from
2161  * the original event are passed through.
2163  * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
2164  *  aggregated  (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
2165  *  A `null` value will remove aggregated events.
2167  * @throws {Error} An error is thrown if aggregation already exists.
2168  */
2169 OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
2170         var i, len, item, add, remove, itemEvent, groupEvent;
2172         for ( itemEvent in events ) {
2173                 groupEvent = events[ itemEvent ];
2175                 // Remove existing aggregated event
2176                 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
2177                         // Don't allow duplicate aggregations
2178                         if ( groupEvent ) {
2179                                 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
2180                         }
2181                         // Remove event aggregation from existing items
2182                         for ( i = 0, len = this.items.length; i < len; i++ ) {
2183                                 item = this.items[ i ];
2184                                 if ( item.connect && item.disconnect ) {
2185                                         remove = {};
2186                                         remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2187                                         item.disconnect( this, remove );
2188                                 }
2189                         }
2190                         // Prevent future items from aggregating event
2191                         delete this.aggregateItemEvents[ itemEvent ];
2192                 }
2194                 // Add new aggregate event
2195                 if ( groupEvent ) {
2196                         // Make future items aggregate event
2197                         this.aggregateItemEvents[ itemEvent ] = groupEvent;
2198                         // Add event aggregation to existing items
2199                         for ( i = 0, len = this.items.length; i < len; i++ ) {
2200                                 item = this.items[ i ];
2201                                 if ( item.connect && item.disconnect ) {
2202                                         add = {};
2203                                         add[ itemEvent ] = [ 'emit', groupEvent, item ];
2204                                         item.connect( this, add );
2205                                 }
2206                         }
2207                 }
2208         }
2212  * Add items to the group.
2214  * Items will be added to the end of the group array unless the optional `index` parameter specifies
2215  * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2217  * @param {OO.ui.Element[]} items An array of items to add to the group
2218  * @param {number} [index] Index of the insertion point
2219  * @chainable
2220  */
2221 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
2222         var i, len, item, event, events, currentIndex,
2223                 itemElements = [];
2225         for ( i = 0, len = items.length; i < len; i++ ) {
2226                 item = items[ i ];
2228                 // Check if item exists then remove it first, effectively "moving" it
2229                 currentIndex = this.items.indexOf( item );
2230                 if ( currentIndex >= 0 ) {
2231                         this.removeItems( [ item ] );
2232                         // Adjust index to compensate for removal
2233                         if ( currentIndex < index ) {
2234                                 index--;
2235                         }
2236                 }
2237                 // Add the item
2238                 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
2239                         events = {};
2240                         for ( event in this.aggregateItemEvents ) {
2241                                 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
2242                         }
2243                         item.connect( this, events );
2244                 }
2245                 item.setElementGroup( this );
2246                 itemElements.push( item.$element.get( 0 ) );
2247         }
2249         if ( index === undefined || index < 0 || index >= this.items.length ) {
2250                 this.$group.append( itemElements );
2251                 this.items.push.apply( this.items, items );
2252         } else if ( index === 0 ) {
2253                 this.$group.prepend( itemElements );
2254                 this.items.unshift.apply( this.items, items );
2255         } else {
2256                 this.items[ index ].$element.before( itemElements );
2257                 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
2258         }
2260         this.emit( 'change', this.getItems() );
2261         return this;
2265  * Remove the specified items from a group.
2267  * Removed items are detached (not removed) from the DOM so that they may be reused.
2268  * To remove all items from a group, you may wish to use the #clearItems method instead.
2270  * @param {OO.ui.Element[]} items An array of items to remove
2271  * @chainable
2272  */
2273 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
2274         var i, len, item, index, remove, itemEvent;
2276         // Remove specific items
2277         for ( i = 0, len = items.length; i < len; i++ ) {
2278                 item = items[ i ];
2279                 index = this.items.indexOf( item );
2280                 if ( index !== -1 ) {
2281                         if (
2282                                 item.connect && item.disconnect &&
2283                                 !$.isEmptyObject( this.aggregateItemEvents )
2284                         ) {
2285                                 remove = {};
2286                                 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
2287                                         remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2288                                 }
2289                                 item.disconnect( this, remove );
2290                         }
2291                         item.setElementGroup( null );
2292                         this.items.splice( index, 1 );
2293                         item.$element.detach();
2294                 }
2295         }
2297         this.emit( 'change', this.getItems() );
2298         return this;
2302  * Clear all items from the group.
2304  * Cleared items are detached from the DOM, not removed, so that they may be reused.
2305  * To remove only a subset of items from a group, use the #removeItems method.
2307  * @chainable
2308  */
2309 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
2310         var i, len, item, remove, itemEvent;
2312         // Remove all items
2313         for ( i = 0, len = this.items.length; i < len; i++ ) {
2314                 item = this.items[ i ];
2315                 if (
2316                         item.connect && item.disconnect &&
2317                         !$.isEmptyObject( this.aggregateItemEvents )
2318                 ) {
2319                         remove = {};
2320                         if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
2321                                 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2322                         }
2323                         item.disconnect( this, remove );
2324                 }
2325                 item.setElementGroup( null );
2326                 item.$element.detach();
2327         }
2329         this.emit( 'change', this.getItems() );
2330         this.items = [];
2331         return this;
2335  * IconElement is often mixed into other classes to generate an icon.
2336  * Icons are graphics, about the size of normal text. They are used to aid the user
2337  * in locating a control or to convey information in a space-efficient way. See the
2338  * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2339  * included in the library.
2341  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2343  * @abstract
2344  * @class
2346  * @constructor
2347  * @param {Object} [config] Configuration options
2348  * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2349  *  the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2350  *  the icon element be set to an existing icon instead of the one generated by this class, set a
2351  *  value using a jQuery selection. For example:
2353  *      // Use a <div> tag instead of a <span>
2354  *     $icon: $("<div>")
2355  *     // Use an existing icon element instead of the one generated by the class
2356  *     $icon: this.$element
2357  *     // Use an icon element from a child widget
2358  *     $icon: this.childwidget.$element
2359  * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2360  *  symbolic names.  A map is used for i18n purposes and contains a `default` icon
2361  *  name and additional names keyed by language code. The `default` name is used when no icon is keyed
2362  *  by the user's language.
2364  *  Example of an i18n map:
2366  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2367  *  See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2368  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2369  * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2370  *  text. The icon title is displayed when users move the mouse over the icon.
2371  */
2372 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
2373         // Configuration initialization
2374         config = config || {};
2376         // Properties
2377         this.$icon = null;
2378         this.icon = null;
2379         this.iconTitle = null;
2381         // Initialization
2382         this.setIcon( config.icon || this.constructor.static.icon );
2383         this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
2384         this.setIconElement( config.$icon || $( '<span>' ) );
2387 /* Setup */
2389 OO.initClass( OO.ui.mixin.IconElement );
2391 /* Static Properties */
2394  * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2395  * for i18n purposes and contains a `default` icon name and additional names keyed by
2396  * language code. The `default` name is used when no icon is keyed by the user's language.
2398  * Example of an i18n map:
2400  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2402  * Note: the static property will be overridden if the #icon configuration is used.
2404  * @static
2405  * @inheritable
2406  * @property {Object|string}
2407  */
2408 OO.ui.mixin.IconElement.static.icon = null;
2411  * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2412  * function that returns title text, or `null` for no title.
2414  * The static property will be overridden if the #iconTitle configuration is used.
2416  * @static
2417  * @inheritable
2418  * @property {string|Function|null}
2419  */
2420 OO.ui.mixin.IconElement.static.iconTitle = null;
2422 /* Methods */
2425  * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2426  * applies to the specified icon element instead of the one created by the class. If an icon
2427  * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2428  * and mixin methods will no longer affect the element.
2430  * @param {jQuery} $icon Element to use as icon
2431  */
2432 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
2433         if ( this.$icon ) {
2434                 this.$icon
2435                         .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
2436                         .removeAttr( 'title' );
2437         }
2439         this.$icon = $icon
2440                 .addClass( 'oo-ui-iconElement-icon' )
2441                 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
2442         if ( this.iconTitle !== null ) {
2443                 this.$icon.attr( 'title', this.iconTitle );
2444         }
2446         this.updateThemeClasses();
2450  * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2451  * The icon parameter can also be set to a map of icon names. See the #icon config setting
2452  * for an example.
2454  * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2455  *  by language code, or `null` to remove the icon.
2456  * @chainable
2457  */
2458 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
2459         icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
2460         icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
2462         if ( this.icon !== icon ) {
2463                 if ( this.$icon ) {
2464                         if ( this.icon !== null ) {
2465                                 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
2466                         }
2467                         if ( icon !== null ) {
2468                                 this.$icon.addClass( 'oo-ui-icon-' + icon );
2469                         }
2470                 }
2471                 this.icon = icon;
2472         }
2474         this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
2475         this.updateThemeClasses();
2477         return this;
2481  * Set the icon title. Use `null` to remove the title.
2483  * @param {string|Function|null} iconTitle A text string used as the icon title,
2484  *  a function that returns title text, or `null` for no title.
2485  * @chainable
2486  */
2487 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
2488         iconTitle = typeof iconTitle === 'function' ||
2489                 ( typeof iconTitle === 'string' && iconTitle.length ) ?
2490                         OO.ui.resolveMsg( iconTitle ) : null;
2492         if ( this.iconTitle !== iconTitle ) {
2493                 this.iconTitle = iconTitle;
2494                 if ( this.$icon ) {
2495                         if ( this.iconTitle !== null ) {
2496                                 this.$icon.attr( 'title', iconTitle );
2497                         } else {
2498                                 this.$icon.removeAttr( 'title' );
2499                         }
2500                 }
2501         }
2503         return this;
2507  * Get the symbolic name of the icon.
2509  * @return {string} Icon name
2510  */
2511 OO.ui.mixin.IconElement.prototype.getIcon = function () {
2512         return this.icon;
2516  * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2518  * @return {string} Icon title text
2519  */
2520 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
2521         return this.iconTitle;
2525  * IndicatorElement is often mixed into other classes to generate an indicator.
2526  * Indicators are small graphics that are generally used in two ways:
2528  * - To draw attention to the status of an item. For example, an indicator might be
2529  *   used to show that an item in a list has errors that need to be resolved.
2530  * - To clarify the function of a control that acts in an exceptional way (a button
2531  *   that opens a menu instead of performing an action directly, for example).
2533  * For a list of indicators included in the library, please see the
2534  * [OOjs UI documentation on MediaWiki] [1].
2536  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2538  * @abstract
2539  * @class
2541  * @constructor
2542  * @param {Object} [config] Configuration options
2543  * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2544  *  configuration is omitted, the indicator element will use a generated `<span>`.
2545  * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
2546  *  See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2547  *  in the library.
2548  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2549  * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2550  *  or a function that returns title text. The indicator title is displayed when users move
2551  *  the mouse over the indicator.
2552  */
2553 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
2554         // Configuration initialization
2555         config = config || {};
2557         // Properties
2558         this.$indicator = null;
2559         this.indicator = null;
2560         this.indicatorTitle = null;
2562         // Initialization
2563         this.setIndicator( config.indicator || this.constructor.static.indicator );
2564         this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
2565         this.setIndicatorElement( config.$indicator || $( '<span>' ) );
2568 /* Setup */
2570 OO.initClass( OO.ui.mixin.IndicatorElement );
2572 /* Static Properties */
2575  * Symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
2576  * The static property will be overridden if the #indicator configuration is used.
2578  * @static
2579  * @inheritable
2580  * @property {string|null}
2581  */
2582 OO.ui.mixin.IndicatorElement.static.indicator = null;
2585  * A text string used as the indicator title, a function that returns title text, or `null`
2586  * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2588  * @static
2589  * @inheritable
2590  * @property {string|Function|null}
2591  */
2592 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
2594 /* Methods */
2597  * Set the indicator element.
2599  * If an element is already set, it will be cleaned up before setting up the new element.
2601  * @param {jQuery} $indicator Element to use as indicator
2602  */
2603 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
2604         if ( this.$indicator ) {
2605                 this.$indicator
2606                         .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
2607                         .removeAttr( 'title' );
2608         }
2610         this.$indicator = $indicator
2611                 .addClass( 'oo-ui-indicatorElement-indicator' )
2612                 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
2613         if ( this.indicatorTitle !== null ) {
2614                 this.$indicator.attr( 'title', this.indicatorTitle );
2615         }
2617         this.updateThemeClasses();
2621  * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2623  * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2624  * @chainable
2625  */
2626 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
2627         indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
2629         if ( this.indicator !== indicator ) {
2630                 if ( this.$indicator ) {
2631                         if ( this.indicator !== null ) {
2632                                 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
2633                         }
2634                         if ( indicator !== null ) {
2635                                 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
2636                         }
2637                 }
2638                 this.indicator = indicator;
2639         }
2641         this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
2642         this.updateThemeClasses();
2644         return this;
2648  * Set the indicator title.
2650  * The title is displayed when a user moves the mouse over the indicator.
2652  * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2653  *   `null` for no indicator title
2654  * @chainable
2655  */
2656 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
2657         indicatorTitle = typeof indicatorTitle === 'function' ||
2658                 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
2659                         OO.ui.resolveMsg( indicatorTitle ) : null;
2661         if ( this.indicatorTitle !== indicatorTitle ) {
2662                 this.indicatorTitle = indicatorTitle;
2663                 if ( this.$indicator ) {
2664                         if ( this.indicatorTitle !== null ) {
2665                                 this.$indicator.attr( 'title', indicatorTitle );
2666                         } else {
2667                                 this.$indicator.removeAttr( 'title' );
2668                         }
2669                 }
2670         }
2672         return this;
2676  * Get the symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
2678  * @return {string} Symbolic name of indicator
2679  */
2680 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
2681         return this.indicator;
2685  * Get the indicator title.
2687  * The title is displayed when a user moves the mouse over the indicator.
2689  * @return {string} Indicator title text
2690  */
2691 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
2692         return this.indicatorTitle;
2696  * LabelElement is often mixed into other classes to generate a label, which
2697  * helps identify the function of an interface element.
2698  * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2700  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2702  * @abstract
2703  * @class
2705  * @constructor
2706  * @param {Object} [config] Configuration options
2707  * @cfg {jQuery} [$label] The label element created by the class. If this
2708  *  configuration is omitted, the label element will use a generated `<span>`.
2709  * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2710  *  as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2711  *  in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2712  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2713  */
2714 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
2715         // Configuration initialization
2716         config = config || {};
2718         // Properties
2719         this.$label = null;
2720         this.label = null;
2722         // Initialization
2723         this.setLabel( config.label || this.constructor.static.label );
2724         this.setLabelElement( config.$label || $( '<span>' ) );
2727 /* Setup */
2729 OO.initClass( OO.ui.mixin.LabelElement );
2731 /* Events */
2734  * @event labelChange
2735  * @param {string} value
2736  */
2738 /* Static Properties */
2741  * The label text. The label can be specified as a plaintext string, a function that will
2742  * produce a string in the future, or `null` for no label. The static value will
2743  * be overridden if a label is specified with the #label config option.
2745  * @static
2746  * @inheritable
2747  * @property {string|Function|null}
2748  */
2749 OO.ui.mixin.LabelElement.static.label = null;
2751 /* Static methods */
2754  * Highlight the first occurrence of the query in the given text
2756  * @param {string} text Text
2757  * @param {string} query Query to find
2758  * @return {jQuery} Text with the first match of the query
2759  *  sub-string wrapped in highlighted span
2760  */
2761 OO.ui.mixin.LabelElement.static.highlightQuery = function ( text, query ) {
2762         var $result = $( '<span>' ),
2763                 offset = text.toLowerCase().indexOf( query.toLowerCase() );
2765         if ( !query.length || offset === -1 ) {
2766                 return $result.text( text );
2767         }
2768         $result.append(
2769                 document.createTextNode( text.slice( 0, offset ) ),
2770                 $( '<span>' )
2771                         .addClass( 'oo-ui-labelElement-label-highlight' )
2772                         .text( text.slice( offset, offset + query.length ) ),
2773                 document.createTextNode( text.slice( offset + query.length ) )
2774         );
2775         return $result.contents();
2778 /* Methods */
2781  * Set the label element.
2783  * If an element is already set, it will be cleaned up before setting up the new element.
2785  * @param {jQuery} $label Element to use as label
2786  */
2787 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
2788         if ( this.$label ) {
2789                 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
2790         }
2792         this.$label = $label.addClass( 'oo-ui-labelElement-label' );
2793         this.setLabelContent( this.label );
2797  * Set the label.
2799  * An empty string will result in the label being hidden. A string containing only whitespace will
2800  * be converted to a single `&nbsp;`.
2802  * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2803  *  text; or null for no label
2804  * @chainable
2805  */
2806 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
2807         label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
2808         label = ( ( typeof label === 'string' || label instanceof jQuery ) && label.length ) || ( label instanceof OO.ui.HtmlSnippet && label.toString().length ) ? label : null;
2810         if ( this.label !== label ) {
2811                 if ( this.$label ) {
2812                         this.setLabelContent( label );
2813                 }
2814                 this.label = label;
2815                 this.emit( 'labelChange' );
2816         }
2818         this.$element.toggleClass( 'oo-ui-labelElement', !!this.label );
2820         return this;
2824  * Set the label as plain text with a highlighted query
2826  * @param {string} text Text label to set
2827  * @param {string} query Substring of text to highlight
2828  * @chainable
2829  */
2830 OO.ui.mixin.LabelElement.prototype.setHighlightedQuery = function ( text, query ) {
2831         return this.setLabel( this.constructor.static.highlightQuery( text, query ) );
2835  * Get the label.
2837  * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2838  *  text; or null for no label
2839  */
2840 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
2841         return this.label;
2845  * Fit the label.
2847  * @chainable
2848  * @deprecated since 0.16.0
2849  */
2850 OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
2851         return this;
2855  * Set the content of the label.
2857  * Do not call this method until after the label element has been set by #setLabelElement.
2859  * @private
2860  * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2861  *  text; or null for no label
2862  */
2863 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
2864         if ( typeof label === 'string' ) {
2865                 if ( label.match( /^\s*$/ ) ) {
2866                         // Convert whitespace only string to a single non-breaking space
2867                         this.$label.html( '&nbsp;' );
2868                 } else {
2869                         this.$label.text( label );
2870                 }
2871         } else if ( label instanceof OO.ui.HtmlSnippet ) {
2872                 this.$label.html( label.toString() );
2873         } else if ( label instanceof jQuery ) {
2874                 this.$label.empty().append( label );
2875         } else {
2876                 this.$label.empty();
2877         }
2881  * The FlaggedElement class is an attribute mixin, meaning that it is used to add
2882  * additional functionality to an element created by another class. The class provides
2883  * a ‘flags’ property assigned the name (or an array of names) of styling flags,
2884  * which are used to customize the look and feel of a widget to better describe its
2885  * importance and functionality.
2887  * The library currently contains the following styling flags for general use:
2889  * - **progressive**:  Progressive styling is applied to convey that the widget will move the user forward in a process.
2890  * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
2891  * - **constructive**: Constructive styling is applied to convey that the widget will create something.
2893  * The flags affect the appearance of the buttons:
2895  *     @example
2896  *     // FlaggedElement is mixed into ButtonWidget to provide styling flags
2897  *     var button1 = new OO.ui.ButtonWidget( {
2898  *         label: 'Constructive',
2899  *         flags: 'constructive'
2900  *     } );
2901  *     var button2 = new OO.ui.ButtonWidget( {
2902  *         label: 'Destructive',
2903  *         flags: 'destructive'
2904  *     } );
2905  *     var button3 = new OO.ui.ButtonWidget( {
2906  *         label: 'Progressive',
2907  *         flags: 'progressive'
2908  *     } );
2909  *     $( 'body' ).append( button1.$element, button2.$element, button3.$element );
2911  * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
2912  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
2914  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2916  * @abstract
2917  * @class
2919  * @constructor
2920  * @param {Object} [config] Configuration options
2921  * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
2922  *  Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
2923  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2924  * @cfg {jQuery} [$flagged] The flagged element. By default,
2925  *  the flagged functionality is applied to the element created by the class ($element).
2926  *  If a different element is specified, the flagged functionality will be applied to it instead.
2927  */
2928 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
2929         // Configuration initialization
2930         config = config || {};
2932         // Properties
2933         this.flags = {};
2934         this.$flagged = null;
2936         // Initialization
2937         this.setFlags( config.flags );
2938         this.setFlaggedElement( config.$flagged || this.$element );
2941 /* Events */
2944  * @event flag
2945  * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
2946  * parameter contains the name of each modified flag and indicates whether it was
2947  * added or removed.
2949  * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
2950  * that the flag was added, `false` that the flag was removed.
2951  */
2953 /* Methods */
2956  * Set the flagged element.
2958  * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
2959  * If an element is already set, the method will remove the mixin’s effect on that element.
2961  * @param {jQuery} $flagged Element that should be flagged
2962  */
2963 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
2964         var classNames = Object.keys( this.flags ).map( function ( flag ) {
2965                 return 'oo-ui-flaggedElement-' + flag;
2966         } ).join( ' ' );
2968         if ( this.$flagged ) {
2969                 this.$flagged.removeClass( classNames );
2970         }
2972         this.$flagged = $flagged.addClass( classNames );
2976  * Check if the specified flag is set.
2978  * @param {string} flag Name of flag
2979  * @return {boolean} The flag is set
2980  */
2981 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
2982         // This may be called before the constructor, thus before this.flags is set
2983         return this.flags && ( flag in this.flags );
2987  * Get the names of all flags set.
2989  * @return {string[]} Flag names
2990  */
2991 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
2992         // This may be called before the constructor, thus before this.flags is set
2993         return Object.keys( this.flags || {} );
2997  * Clear all flags.
2999  * @chainable
3000  * @fires flag
3001  */
3002 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
3003         var flag, className,
3004                 changes = {},
3005                 remove = [],
3006                 classPrefix = 'oo-ui-flaggedElement-';
3008         for ( flag in this.flags ) {
3009                 className = classPrefix + flag;
3010                 changes[ flag ] = false;
3011                 delete this.flags[ flag ];
3012                 remove.push( className );
3013         }
3015         if ( this.$flagged ) {
3016                 this.$flagged.removeClass( remove.join( ' ' ) );
3017         }
3019         this.updateThemeClasses();
3020         this.emit( 'flag', changes );
3022         return this;
3026  * Add one or more flags.
3028  * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3029  *  or an object keyed by flag name with a boolean value that indicates whether the flag should
3030  *  be added (`true`) or removed (`false`).
3031  * @chainable
3032  * @fires flag
3033  */
3034 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
3035         var i, len, flag, className,
3036                 changes = {},
3037                 add = [],
3038                 remove = [],
3039                 classPrefix = 'oo-ui-flaggedElement-';
3041         if ( typeof flags === 'string' ) {
3042                 className = classPrefix + flags;
3043                 // Set
3044                 if ( !this.flags[ flags ] ) {
3045                         this.flags[ flags ] = true;
3046                         add.push( className );
3047                 }
3048         } else if ( Array.isArray( flags ) ) {
3049                 for ( i = 0, len = flags.length; i < len; i++ ) {
3050                         flag = flags[ i ];
3051                         className = classPrefix + flag;
3052                         // Set
3053                         if ( !this.flags[ flag ] ) {
3054                                 changes[ flag ] = true;
3055                                 this.flags[ flag ] = true;
3056                                 add.push( className );
3057                         }
3058                 }
3059         } else if ( OO.isPlainObject( flags ) ) {
3060                 for ( flag in flags ) {
3061                         className = classPrefix + flag;
3062                         if ( flags[ flag ] ) {
3063                                 // Set
3064                                 if ( !this.flags[ flag ] ) {
3065                                         changes[ flag ] = true;
3066                                         this.flags[ flag ] = true;
3067                                         add.push( className );
3068                                 }
3069                         } else {
3070                                 // Remove
3071                                 if ( this.flags[ flag ] ) {
3072                                         changes[ flag ] = false;
3073                                         delete this.flags[ flag ];
3074                                         remove.push( className );
3075                                 }
3076                         }
3077                 }
3078         }
3080         if ( this.$flagged ) {
3081                 this.$flagged
3082                         .addClass( add.join( ' ' ) )
3083                         .removeClass( remove.join( ' ' ) );
3084         }
3086         this.updateThemeClasses();
3087         this.emit( 'flag', changes );
3089         return this;
3093  * TitledElement is mixed into other classes to provide a `title` attribute.
3094  * Titles are rendered by the browser and are made visible when the user moves
3095  * the mouse over the element. Titles are not visible on touch devices.
3097  *     @example
3098  *     // TitledElement provides a 'title' attribute to the
3099  *     // ButtonWidget class
3100  *     var button = new OO.ui.ButtonWidget( {
3101  *         label: 'Button with Title',
3102  *         title: 'I am a button'
3103  *     } );
3104  *     $( 'body' ).append( button.$element );
3106  * @abstract
3107  * @class
3109  * @constructor
3110  * @param {Object} [config] Configuration options
3111  * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3112  *  If this config is omitted, the title functionality is applied to $element, the
3113  *  element created by the class.
3114  * @cfg {string|Function} [title] The title text or a function that returns text. If
3115  *  this config is omitted, the value of the {@link #static-title static title} property is used.
3116  */
3117 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
3118         // Configuration initialization
3119         config = config || {};
3121         // Properties
3122         this.$titled = null;
3123         this.title = null;
3125         // Initialization
3126         this.setTitle( config.title !== undefined ? config.title : this.constructor.static.title );
3127         this.setTitledElement( config.$titled || this.$element );
3130 /* Setup */
3132 OO.initClass( OO.ui.mixin.TitledElement );
3134 /* Static Properties */
3137  * The title text, a function that returns text, or `null` for no title. The value of the static property
3138  * is overridden if the #title config option is used.
3140  * @static
3141  * @inheritable
3142  * @property {string|Function|null}
3143  */
3144 OO.ui.mixin.TitledElement.static.title = null;
3146 /* Methods */
3149  * Set the titled element.
3151  * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3152  * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3154  * @param {jQuery} $titled Element that should use the 'titled' functionality
3155  */
3156 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
3157         if ( this.$titled ) {
3158                 this.$titled.removeAttr( 'title' );
3159         }
3161         this.$titled = $titled;
3162         if ( this.title ) {
3163                 this.$titled.attr( 'title', this.title );
3164         }
3168  * Set title.
3170  * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3171  * @chainable
3172  */
3173 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
3174         title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
3175         title = ( typeof title === 'string' && title.length ) ? title : null;
3177         if ( this.title !== title ) {
3178                 if ( this.$titled ) {
3179                         if ( title !== null ) {
3180                                 this.$titled.attr( 'title', title );
3181                         } else {
3182                                 this.$titled.removeAttr( 'title' );
3183                         }
3184                 }
3185                 this.title = title;
3186         }
3188         return this;
3192  * Get title.
3194  * @return {string} Title string
3195  */
3196 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
3197         return this.title;
3201  * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3202  * Accesskeys allow an user to go to a specific element by using
3203  * a shortcut combination of a browser specific keys + the key
3204  * set to the field.
3206  *     @example
3207  *     // AccessKeyedElement provides an 'accesskey' attribute to the
3208  *     // ButtonWidget class
3209  *     var button = new OO.ui.ButtonWidget( {
3210  *         label: 'Button with Accesskey',
3211  *         accessKey: 'k'
3212  *     } );
3213  *     $( 'body' ).append( button.$element );
3215  * @abstract
3216  * @class
3218  * @constructor
3219  * @param {Object} [config] Configuration options
3220  * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3221  *  If this config is omitted, the accesskey functionality is applied to $element, the
3222  *  element created by the class.
3223  * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3224  *  this config is omitted, no accesskey will be added.
3225  */
3226 OO.ui.mixin.AccessKeyedElement = function OoUiMixinAccessKeyedElement( config ) {
3227         // Configuration initialization
3228         config = config || {};
3230         // Properties
3231         this.$accessKeyed = null;
3232         this.accessKey = null;
3234         // Initialization
3235         this.setAccessKey( config.accessKey || null );
3236         this.setAccessKeyedElement( config.$accessKeyed || this.$element );
3239 /* Setup */
3241 OO.initClass( OO.ui.mixin.AccessKeyedElement );
3243 /* Static Properties */
3246  * The access key, a function that returns a key, or `null` for no accesskey.
3248  * @static
3249  * @inheritable
3250  * @property {string|Function|null}
3251  */
3252 OO.ui.mixin.AccessKeyedElement.static.accessKey = null;
3254 /* Methods */
3257  * Set the accesskeyed element.
3259  * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3260  * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3262  * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3263  */
3264 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKeyedElement = function ( $accessKeyed ) {
3265         if ( this.$accessKeyed ) {
3266                 this.$accessKeyed.removeAttr( 'accesskey' );
3267         }
3269         this.$accessKeyed = $accessKeyed;
3270         if ( this.accessKey ) {
3271                 this.$accessKeyed.attr( 'accesskey', this.accessKey );
3272         }
3276  * Set accesskey.
3278  * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3279  * @chainable
3280  */
3281 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKey = function ( accessKey ) {
3282         accessKey = typeof accessKey === 'string' ? OO.ui.resolveMsg( accessKey ) : null;
3284         if ( this.accessKey !== accessKey ) {
3285                 if ( this.$accessKeyed ) {
3286                         if ( accessKey !== null ) {
3287                                 this.$accessKeyed.attr( 'accesskey', accessKey );
3288                         } else {
3289                                 this.$accessKeyed.removeAttr( 'accesskey' );
3290                         }
3291                 }
3292                 this.accessKey = accessKey;
3293         }
3295         return this;
3299  * Get accesskey.
3301  * @return {string} accessKey string
3302  */
3303 OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
3304         return this.accessKey;
3308  * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3309  * feels, and functionality can be customized via the class’s configuration options
3310  * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3311  * and examples.
3313  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3315  *     @example
3316  *     // A button widget
3317  *     var button = new OO.ui.ButtonWidget( {
3318  *         label: 'Button with Icon',
3319  *         icon: 'remove',
3320  *         iconTitle: 'Remove'
3321  *     } );
3322  *     $( 'body' ).append( button.$element );
3324  * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3326  * @class
3327  * @extends OO.ui.Widget
3328  * @mixins OO.ui.mixin.ButtonElement
3329  * @mixins OO.ui.mixin.IconElement
3330  * @mixins OO.ui.mixin.IndicatorElement
3331  * @mixins OO.ui.mixin.LabelElement
3332  * @mixins OO.ui.mixin.TitledElement
3333  * @mixins OO.ui.mixin.FlaggedElement
3334  * @mixins OO.ui.mixin.TabIndexedElement
3335  * @mixins OO.ui.mixin.AccessKeyedElement
3337  * @constructor
3338  * @param {Object} [config] Configuration options
3339  * @cfg {boolean} [active=false] Whether button should be shown as active
3340  * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3341  * @cfg {string} [target] The frame or window in which to open the hyperlink.
3342  * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3343  */
3344 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
3345         // Configuration initialization
3346         config = config || {};
3348         // Parent constructor
3349         OO.ui.ButtonWidget.parent.call( this, config );
3351         // Mixin constructors
3352         OO.ui.mixin.ButtonElement.call( this, config );
3353         OO.ui.mixin.IconElement.call( this, config );
3354         OO.ui.mixin.IndicatorElement.call( this, config );
3355         OO.ui.mixin.LabelElement.call( this, config );
3356         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
3357         OO.ui.mixin.FlaggedElement.call( this, config );
3358         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
3359         OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$button } ) );
3361         // Properties
3362         this.href = null;
3363         this.target = null;
3364         this.noFollow = false;
3366         // Events
3367         this.connect( this, { disable: 'onDisable' } );
3369         // Initialization
3370         this.$button.append( this.$icon, this.$label, this.$indicator );
3371         this.$element
3372                 .addClass( 'oo-ui-buttonWidget' )
3373                 .append( this.$button );
3374         this.setActive( config.active );
3375         this.setHref( config.href );
3376         this.setTarget( config.target );
3377         this.setNoFollow( config.noFollow );
3380 /* Setup */
3382 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
3383 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
3384 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
3385 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
3386 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
3387 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
3388 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
3389 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
3390 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
3392 /* Methods */
3395  * @inheritdoc
3396  */
3397 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
3398         if ( !this.isDisabled() ) {
3399                 // Remove the tab-index while the button is down to prevent the button from stealing focus
3400                 this.$button.removeAttr( 'tabindex' );
3401         }
3403         return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
3407  * @inheritdoc
3408  */
3409 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
3410         if ( !this.isDisabled() ) {
3411                 // Restore the tab-index after the button is up to restore the button's accessibility
3412                 this.$button.attr( 'tabindex', this.tabIndex );
3413         }
3415         return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
3419  * Get hyperlink location.
3421  * @return {string} Hyperlink location
3422  */
3423 OO.ui.ButtonWidget.prototype.getHref = function () {
3424         return this.href;
3428  * Get hyperlink target.
3430  * @return {string} Hyperlink target
3431  */
3432 OO.ui.ButtonWidget.prototype.getTarget = function () {
3433         return this.target;
3437  * Get search engine traversal hint.
3439  * @return {boolean} Whether search engines should avoid traversing this hyperlink
3440  */
3441 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
3442         return this.noFollow;
3446  * Set hyperlink location.
3448  * @param {string|null} href Hyperlink location, null to remove
3449  */
3450 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
3451         href = typeof href === 'string' ? href : null;
3452         if ( href !== null && !OO.ui.isSafeUrl( href ) ) {
3453                 href = './' + href;
3454         }
3456         if ( href !== this.href ) {
3457                 this.href = href;
3458                 this.updateHref();
3459         }
3461         return this;
3465  * Update the `href` attribute, in case of changes to href or
3466  * disabled state.
3468  * @private
3469  * @chainable
3470  */
3471 OO.ui.ButtonWidget.prototype.updateHref = function () {
3472         if ( this.href !== null && !this.isDisabled() ) {
3473                 this.$button.attr( 'href', this.href );
3474         } else {
3475                 this.$button.removeAttr( 'href' );
3476         }
3478         return this;
3482  * Handle disable events.
3484  * @private
3485  * @param {boolean} disabled Element is disabled
3486  */
3487 OO.ui.ButtonWidget.prototype.onDisable = function () {
3488         this.updateHref();
3492  * Set hyperlink target.
3494  * @param {string|null} target Hyperlink target, null to remove
3495  */
3496 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
3497         target = typeof target === 'string' ? target : null;
3499         if ( target !== this.target ) {
3500                 this.target = target;
3501                 if ( target !== null ) {
3502                         this.$button.attr( 'target', target );
3503                 } else {
3504                         this.$button.removeAttr( 'target' );
3505                 }
3506         }
3508         return this;
3512  * Set search engine traversal hint.
3514  * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3515  */
3516 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
3517         noFollow = typeof noFollow === 'boolean' ? noFollow : true;
3519         if ( noFollow !== this.noFollow ) {
3520                 this.noFollow = noFollow;
3521                 if ( noFollow ) {
3522                         this.$button.attr( 'rel', 'nofollow' );
3523                 } else {
3524                         this.$button.removeAttr( 'rel' );
3525                 }
3526         }
3528         return this;
3531 // Override method visibility hints from ButtonElement
3533  * @method setActive
3534  */
3536  * @method isActive
3537  */
3540  * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3541  * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3542  * removed, and cleared from the group.
3544  *     @example
3545  *     // Example: A ButtonGroupWidget with two buttons
3546  *     var button1 = new OO.ui.PopupButtonWidget( {
3547  *         label: 'Select a category',
3548  *         icon: 'menu',
3549  *         popup: {
3550  *             $content: $( '<p>List of categories...</p>' ),
3551  *             padded: true,
3552  *             align: 'left'
3553  *         }
3554  *     } );
3555  *     var button2 = new OO.ui.ButtonWidget( {
3556  *         label: 'Add item'
3557  *     });
3558  *     var buttonGroup = new OO.ui.ButtonGroupWidget( {
3559  *         items: [button1, button2]
3560  *     } );
3561  *     $( 'body' ).append( buttonGroup.$element );
3563  * @class
3564  * @extends OO.ui.Widget
3565  * @mixins OO.ui.mixin.GroupElement
3567  * @constructor
3568  * @param {Object} [config] Configuration options
3569  * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3570  */
3571 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
3572         // Configuration initialization
3573         config = config || {};
3575         // Parent constructor
3576         OO.ui.ButtonGroupWidget.parent.call( this, config );
3578         // Mixin constructors
3579         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
3581         // Initialization
3582         this.$element.addClass( 'oo-ui-buttonGroupWidget' );
3583         if ( Array.isArray( config.items ) ) {
3584                 this.addItems( config.items );
3585         }
3588 /* Setup */
3590 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
3591 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
3594  * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3595  * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3596  * for a list of icons included in the library.
3598  *     @example
3599  *     // An icon widget with a label
3600  *     var myIcon = new OO.ui.IconWidget( {
3601  *         icon: 'help',
3602  *         iconTitle: 'Help'
3603  *      } );
3604  *      // Create a label.
3605  *      var iconLabel = new OO.ui.LabelWidget( {
3606  *          label: 'Help'
3607  *      } );
3608  *      $( 'body' ).append( myIcon.$element, iconLabel.$element );
3610  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3612  * @class
3613  * @extends OO.ui.Widget
3614  * @mixins OO.ui.mixin.IconElement
3615  * @mixins OO.ui.mixin.TitledElement
3616  * @mixins OO.ui.mixin.FlaggedElement
3618  * @constructor
3619  * @param {Object} [config] Configuration options
3620  */
3621 OO.ui.IconWidget = function OoUiIconWidget( config ) {
3622         // Configuration initialization
3623         config = config || {};
3625         // Parent constructor
3626         OO.ui.IconWidget.parent.call( this, config );
3628         // Mixin constructors
3629         OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
3630         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
3631         OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
3633         // Initialization
3634         this.$element.addClass( 'oo-ui-iconWidget' );
3637 /* Setup */
3639 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
3640 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
3641 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
3642 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
3644 /* Static Properties */
3646 OO.ui.IconWidget.static.tagName = 'span';
3649  * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3650  * attention to the status of an item or to clarify the function of a control. For a list of
3651  * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3653  *     @example
3654  *     // Example of an indicator widget
3655  *     var indicator1 = new OO.ui.IndicatorWidget( {
3656  *         indicator: 'alert'
3657  *     } );
3659  *     // Create a fieldset layout to add a label
3660  *     var fieldset = new OO.ui.FieldsetLayout();
3661  *     fieldset.addItems( [
3662  *         new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3663  *     ] );
3664  *     $( 'body' ).append( fieldset.$element );
3666  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3668  * @class
3669  * @extends OO.ui.Widget
3670  * @mixins OO.ui.mixin.IndicatorElement
3671  * @mixins OO.ui.mixin.TitledElement
3673  * @constructor
3674  * @param {Object} [config] Configuration options
3675  */
3676 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
3677         // Configuration initialization
3678         config = config || {};
3680         // Parent constructor
3681         OO.ui.IndicatorWidget.parent.call( this, config );
3683         // Mixin constructors
3684         OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
3685         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
3687         // Initialization
3688         this.$element.addClass( 'oo-ui-indicatorWidget' );
3691 /* Setup */
3693 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
3694 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
3695 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
3697 /* Static Properties */
3699 OO.ui.IndicatorWidget.static.tagName = 'span';
3702  * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3703  * be configured with a `label` option that is set to a string, a label node, or a function:
3705  * - String: a plaintext string
3706  * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3707  *   label that includes a link or special styling, such as a gray color or additional graphical elements.
3708  * - Function: a function that will produce a string in the future. Functions are used
3709  *   in cases where the value of the label is not currently defined.
3711  * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3712  * will come into focus when the label is clicked.
3714  *     @example
3715  *     // Examples of LabelWidgets
3716  *     var label1 = new OO.ui.LabelWidget( {
3717  *         label: 'plaintext label'
3718  *     } );
3719  *     var label2 = new OO.ui.LabelWidget( {
3720  *         label: $( '<a href="default.html">jQuery label</a>' )
3721  *     } );
3722  *     // Create a fieldset layout with fields for each example
3723  *     var fieldset = new OO.ui.FieldsetLayout();
3724  *     fieldset.addItems( [
3725  *         new OO.ui.FieldLayout( label1 ),
3726  *         new OO.ui.FieldLayout( label2 )
3727  *     ] );
3728  *     $( 'body' ).append( fieldset.$element );
3730  * @class
3731  * @extends OO.ui.Widget
3732  * @mixins OO.ui.mixin.LabelElement
3734  * @constructor
3735  * @param {Object} [config] Configuration options
3736  * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
3737  *  Clicking the label will focus the specified input field.
3738  */
3739 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
3740         // Configuration initialization
3741         config = config || {};
3743         // Parent constructor
3744         OO.ui.LabelWidget.parent.call( this, config );
3746         // Mixin constructors
3747         OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
3748         OO.ui.mixin.TitledElement.call( this, config );
3750         // Properties
3751         this.input = config.input;
3753         // Events
3754         if ( this.input instanceof OO.ui.InputWidget ) {
3755                 this.$element.on( 'click', this.onClick.bind( this ) );
3756         }
3758         // Initialization
3759         this.$element.addClass( 'oo-ui-labelWidget' );
3762 /* Setup */
3764 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
3765 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
3766 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
3768 /* Static Properties */
3770 OO.ui.LabelWidget.static.tagName = 'span';
3772 /* Methods */
3775  * Handles label mouse click events.
3777  * @private
3778  * @param {jQuery.Event} e Mouse click event
3779  */
3780 OO.ui.LabelWidget.prototype.onClick = function () {
3781         this.input.simulateLabelClick();
3782         return false;
3786  * PendingElement is a mixin that is used to create elements that notify users that something is happening
3787  * and that they should wait before proceeding. The pending state is visually represented with a pending
3788  * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
3789  * field of a {@link OO.ui.TextInputWidget text input widget}.
3791  * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
3792  * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
3793  * in process dialogs.
3795  *     @example
3796  *     function MessageDialog( config ) {
3797  *         MessageDialog.parent.call( this, config );
3798  *     }
3799  *     OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
3801  *     MessageDialog.static.actions = [
3802  *         { action: 'save', label: 'Done', flags: 'primary' },
3803  *         { label: 'Cancel', flags: 'safe' }
3804  *     ];
3806  *     MessageDialog.prototype.initialize = function () {
3807  *         MessageDialog.parent.prototype.initialize.apply( this, arguments );
3808  *         this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
3809  *         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>' );
3810  *         this.$body.append( this.content.$element );
3811  *     };
3812  *     MessageDialog.prototype.getBodyHeight = function () {
3813  *         return 100;
3814  *     }
3815  *     MessageDialog.prototype.getActionProcess = function ( action ) {
3816  *         var dialog = this;
3817  *         if ( action === 'save' ) {
3818  *             dialog.getActions().get({actions: 'save'})[0].pushPending();
3819  *             return new OO.ui.Process()
3820  *             .next( 1000 )
3821  *             .next( function () {
3822  *                 dialog.getActions().get({actions: 'save'})[0].popPending();
3823  *             } );
3824  *         }
3825  *         return MessageDialog.parent.prototype.getActionProcess.call( this, action );
3826  *     };
3828  *     var windowManager = new OO.ui.WindowManager();
3829  *     $( 'body' ).append( windowManager.$element );
3831  *     var dialog = new MessageDialog();
3832  *     windowManager.addWindows( [ dialog ] );
3833  *     windowManager.openWindow( dialog );
3835  * @abstract
3836  * @class
3838  * @constructor
3839  * @param {Object} [config] Configuration options
3840  * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
3841  */
3842 OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
3843         // Configuration initialization
3844         config = config || {};
3846         // Properties
3847         this.pending = 0;
3848         this.$pending = null;
3850         // Initialisation
3851         this.setPendingElement( config.$pending || this.$element );
3854 /* Setup */
3856 OO.initClass( OO.ui.mixin.PendingElement );
3858 /* Methods */
3861  * Set the pending element (and clean up any existing one).
3863  * @param {jQuery} $pending The element to set to pending.
3864  */
3865 OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
3866         if ( this.$pending ) {
3867                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
3868         }
3870         this.$pending = $pending;
3871         if ( this.pending > 0 ) {
3872                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
3873         }
3877  * Check if an element is pending.
3879  * @return {boolean} Element is pending
3880  */
3881 OO.ui.mixin.PendingElement.prototype.isPending = function () {
3882         return !!this.pending;
3886  * Increase the pending counter. The pending state will remain active until the counter is zero
3887  * (i.e., the number of calls to #pushPending and #popPending is the same).
3889  * @chainable
3890  */
3891 OO.ui.mixin.PendingElement.prototype.pushPending = function () {
3892         if ( this.pending === 0 ) {
3893                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
3894                 this.updateThemeClasses();
3895         }
3896         this.pending++;
3898         return this;
3902  * Decrease the pending counter. The pending state will remain active until the counter is zero
3903  * (i.e., the number of calls to #pushPending and #popPending is the same).
3905  * @chainable
3906  */
3907 OO.ui.mixin.PendingElement.prototype.popPending = function () {
3908         if ( this.pending === 1 ) {
3909                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
3910                 this.updateThemeClasses();
3911         }
3912         this.pending = Math.max( 0, this.pending - 1 );
3914         return this;
3918  * Element that can be automatically clipped to visible boundaries.
3920  * Whenever the element's natural height changes, you have to call
3921  * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
3922  * clipping correctly.
3924  * The dimensions of #$clippableContainer will be compared to the boundaries of the
3925  * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
3926  * then #$clippable will be given a fixed reduced height and/or width and will be made
3927  * scrollable. By default, #$clippable and #$clippableContainer are the same element,
3928  * but you can build a static footer by setting #$clippableContainer to an element that contains
3929  * #$clippable and the footer.
3931  * @abstract
3932  * @class
3934  * @constructor
3935  * @param {Object} [config] Configuration options
3936  * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
3937  * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
3938  *   omit to use #$clippable
3939  */
3940 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
3941         // Configuration initialization
3942         config = config || {};
3944         // Properties
3945         this.$clippable = null;
3946         this.$clippableContainer = null;
3947         this.clipping = false;
3948         this.clippedHorizontally = false;
3949         this.clippedVertically = false;
3950         this.$clippableScrollableContainer = null;
3951         this.$clippableScroller = null;
3952         this.$clippableWindow = null;
3953         this.idealWidth = null;
3954         this.idealHeight = null;
3955         this.onClippableScrollHandler = this.clip.bind( this );
3956         this.onClippableWindowResizeHandler = this.clip.bind( this );
3958         // Initialization
3959         if ( config.$clippableContainer ) {
3960                 this.setClippableContainer( config.$clippableContainer );
3961         }
3962         this.setClippableElement( config.$clippable || this.$element );
3965 /* Methods */
3968  * Set clippable element.
3970  * If an element is already set, it will be cleaned up before setting up the new element.
3972  * @param {jQuery} $clippable Element to make clippable
3973  */
3974 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
3975         if ( this.$clippable ) {
3976                 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
3977                 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
3978                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
3979         }
3981         this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
3982         this.clip();
3986  * Set clippable container.
3988  * This is the container that will be measured when deciding whether to clip. When clipping,
3989  * #$clippable will be resized in order to keep the clippable container fully visible.
3991  * If the clippable container is unset, #$clippable will be used.
3993  * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
3994  */
3995 OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
3996         this.$clippableContainer = $clippableContainer;
3997         if ( this.$clippable ) {
3998                 this.clip();
3999         }
4003  * Toggle clipping.
4005  * Do not turn clipping on until after the element is attached to the DOM and visible.
4007  * @param {boolean} [clipping] Enable clipping, omit to toggle
4008  * @chainable
4009  */
4010 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
4011         clipping = clipping === undefined ? !this.clipping : !!clipping;
4013         if ( this.clipping !== clipping ) {
4014                 this.clipping = clipping;
4015                 if ( clipping ) {
4016                         this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
4017                         // If the clippable container is the root, we have to listen to scroll events and check
4018                         // jQuery.scrollTop on the window because of browser inconsistencies
4019                         this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
4020                                 $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
4021                                 this.$clippableScrollableContainer;
4022                         this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
4023                         this.$clippableWindow = $( this.getElementWindow() )
4024                                 .on( 'resize', this.onClippableWindowResizeHandler );
4025                         // Initial clip after visible
4026                         this.clip();
4027                 } else {
4028                         this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
4029                         OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
4031                         this.$clippableScrollableContainer = null;
4032                         this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
4033                         this.$clippableScroller = null;
4034                         this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
4035                         this.$clippableWindow = null;
4036                 }
4037         }
4039         return this;
4043  * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4045  * @return {boolean} Element will be clipped to the visible area
4046  */
4047 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
4048         return this.clipping;
4052  * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4054  * @return {boolean} Part of the element is being clipped
4055  */
4056 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
4057         return this.clippedHorizontally || this.clippedVertically;
4061  * Check if the right of the element is being clipped by the nearest scrollable container.
4063  * @return {boolean} Part of the element is being clipped
4064  */
4065 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
4066         return this.clippedHorizontally;
4070  * Check if the bottom of the element is being clipped by the nearest scrollable container.
4072  * @return {boolean} Part of the element is being clipped
4073  */
4074 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
4075         return this.clippedVertically;
4079  * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
4081  * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4082  * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4083  */
4084 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
4085         this.idealWidth = width;
4086         this.idealHeight = height;
4088         if ( !this.clipping ) {
4089                 // Update dimensions
4090                 this.$clippable.css( { width: width, height: height } );
4091         }
4092         // While clipping, idealWidth and idealHeight are not considered
4096  * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4097  * when the element's natural height changes.
4099  * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4100  * overlapped by, the visible area of the nearest scrollable container.
4102  * Because calling clip() when the natural height changes isn't always possible, we also set
4103  * max-height when the element isn't being clipped. This means that if the element tries to grow
4104  * beyond the edge, something reasonable will happen before clip() is called.
4106  * @chainable
4107  */
4108 OO.ui.mixin.ClippableElement.prototype.clip = function () {
4109         var $container, extraHeight, extraWidth, ccOffset,
4110                 $scrollableContainer, scOffset, scHeight, scWidth,
4111                 ccWidth, scrollerIsWindow, scrollTop, scrollLeft,
4112                 desiredWidth, desiredHeight, allotedWidth, allotedHeight,
4113                 naturalWidth, naturalHeight, clipWidth, clipHeight,
4114                 buffer = 7; // Chosen by fair dice roll
4116         if ( !this.clipping ) {
4117                 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4118                 return this;
4119         }
4121         $container = this.$clippableContainer || this.$clippable;
4122         extraHeight = $container.outerHeight() - this.$clippable.outerHeight();
4123         extraWidth = $container.outerWidth() - this.$clippable.outerWidth();
4124         ccOffset = $container.offset();
4125         $scrollableContainer = this.$clippableScrollableContainer.is( 'html, body' ) ?
4126                 this.$clippableWindow : this.$clippableScrollableContainer;
4127         scOffset = $scrollableContainer.offset() || { top: 0, left: 0 };
4128         scHeight = $scrollableContainer.innerHeight() - buffer;
4129         scWidth = $scrollableContainer.innerWidth() - buffer;
4130         ccWidth = $container.outerWidth() + buffer;
4131         scrollerIsWindow = this.$clippableScroller[ 0 ] === this.$clippableWindow[ 0 ];
4132         scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0;
4133         scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0;
4134         desiredWidth = ccOffset.left < 0 ?
4135                 ccWidth + ccOffset.left :
4136                 ( scOffset.left + scrollLeft + scWidth ) - ccOffset.left;
4137         desiredHeight = ( scOffset.top + scrollTop + scHeight ) - ccOffset.top;
4138         // It should never be desirable to exceed the dimensions of the browser viewport... right?
4139         desiredWidth = Math.min( desiredWidth, document.documentElement.clientWidth );
4140         desiredHeight = Math.min( desiredHeight, document.documentElement.clientHeight );
4141         allotedWidth = Math.ceil( desiredWidth - extraWidth );
4142         allotedHeight = Math.ceil( desiredHeight - extraHeight );
4143         naturalWidth = this.$clippable.prop( 'scrollWidth' );
4144         naturalHeight = this.$clippable.prop( 'scrollHeight' );
4145         clipWidth = allotedWidth < naturalWidth;
4146         clipHeight = allotedHeight < naturalHeight;
4148         if ( clipWidth ) {
4149                 this.$clippable.css( {
4150                         overflowX: 'scroll',
4151                         width: Math.max( 0, allotedWidth ),
4152                         maxWidth: ''
4153                 } );
4154         } else {
4155                 this.$clippable.css( {
4156                         overflowX: '',
4157                         width: this.idealWidth ? this.idealWidth - extraWidth : '',
4158                         maxWidth: Math.max( 0, allotedWidth )
4159                 } );
4160         }
4161         if ( clipHeight ) {
4162                 this.$clippable.css( {
4163                         overflowY: 'scroll',
4164                         height: Math.max( 0, allotedHeight ),
4165                         maxHeight: ''
4166                 } );
4167         } else {
4168                 this.$clippable.css( {
4169                         overflowY: '',
4170                         height: this.idealHeight ? this.idealHeight - extraHeight : '',
4171                         maxHeight: Math.max( 0, allotedHeight )
4172                 } );
4173         }
4175         // If we stopped clipping in at least one of the dimensions
4176         if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
4177                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
4178         }
4180         this.clippedHorizontally = clipWidth;
4181         this.clippedVertically = clipHeight;
4183         return this;
4187  * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4188  * By default, each popup has an anchor that points toward its origin.
4189  * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4191  *     @example
4192  *     // A popup widget.
4193  *     var popup = new OO.ui.PopupWidget( {
4194  *         $content: $( '<p>Hi there!</p>' ),
4195  *         padded: true,
4196  *         width: 300
4197  *     } );
4199  *     $( 'body' ).append( popup.$element );
4200  *     // To display the popup, toggle the visibility to 'true'.
4201  *     popup.toggle( true );
4203  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4205  * @class
4206  * @extends OO.ui.Widget
4207  * @mixins OO.ui.mixin.LabelElement
4208  * @mixins OO.ui.mixin.ClippableElement
4210  * @constructor
4211  * @param {Object} [config] Configuration options
4212  * @cfg {number} [width=320] Width of popup in pixels
4213  * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4214  * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4215  * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
4216  *  If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
4217  *  popup is leaning towards the right of the screen.
4218  *  Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
4219  *  in the given language, which means it will flip to the correct positioning in right-to-left languages.
4220  *  Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
4221  *  sentence in the given language.
4222  * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4223  *  See the [OOjs UI docs on MediaWiki][3] for an example.
4224  *  [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4225  * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4226  * @cfg {jQuery} [$content] Content to append to the popup's body
4227  * @cfg {jQuery} [$footer] Content to append to the popup's footer
4228  * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4229  * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4230  *  This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4231  *  for an example.
4232  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4233  * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
4234  *  button.
4235  * @cfg {boolean} [padded=false] Add padding to the popup's body
4236  */
4237 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
4238         // Configuration initialization
4239         config = config || {};
4241         // Parent constructor
4242         OO.ui.PopupWidget.parent.call( this, config );
4244         // Properties (must be set before ClippableElement constructor call)
4245         this.$body = $( '<div>' );
4246         this.$popup = $( '<div>' );
4248         // Mixin constructors
4249         OO.ui.mixin.LabelElement.call( this, config );
4250         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, {
4251                 $clippable: this.$body,
4252                 $clippableContainer: this.$popup
4253         } ) );
4255         // Properties
4256         this.$anchor = $( '<div>' );
4257         // If undefined, will be computed lazily in updateDimensions()
4258         this.$container = config.$container;
4259         this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
4260         this.autoClose = !!config.autoClose;
4261         this.$autoCloseIgnore = config.$autoCloseIgnore;
4262         this.transitionTimeout = null;
4263         this.anchor = null;
4264         this.width = config.width !== undefined ? config.width : 320;
4265         this.height = config.height !== undefined ? config.height : null;
4266         this.setAlignment( config.align );
4267         this.onMouseDownHandler = this.onMouseDown.bind( this );
4268         this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
4270         // Initialization
4271         this.toggleAnchor( config.anchor === undefined || config.anchor );
4272         this.$body.addClass( 'oo-ui-popupWidget-body' );
4273         this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
4274         this.$popup
4275                 .addClass( 'oo-ui-popupWidget-popup' )
4276                 .append( this.$body );
4277         this.$element
4278                 .addClass( 'oo-ui-popupWidget' )
4279                 .append( this.$popup, this.$anchor );
4280         // Move content, which was added to #$element by OO.ui.Widget, to the body
4281         // FIXME This is gross, we should use '$body' or something for the config
4282         if ( config.$content instanceof jQuery ) {
4283                 this.$body.append( config.$content );
4284         }
4286         if ( config.padded ) {
4287                 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
4288         }
4290         if ( config.head ) {
4291                 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
4292                 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
4293                 this.$head = $( '<div>' )
4294                         .addClass( 'oo-ui-popupWidget-head' )
4295                         .append( this.$label, this.closeButton.$element );
4296                 this.$popup.prepend( this.$head );
4297         }
4299         if ( config.$footer ) {
4300                 this.$footer = $( '<div>' )
4301                         .addClass( 'oo-ui-popupWidget-footer' )
4302                         .append( config.$footer );
4303                 this.$popup.append( this.$footer );
4304         }
4306         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
4307         // that reference properties not initialized at that time of parent class construction
4308         // TODO: Find a better way to handle post-constructor setup
4309         this.visible = false;
4310         this.$element.addClass( 'oo-ui-element-hidden' );
4313 /* Setup */
4315 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
4316 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
4317 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
4319 /* Methods */
4322  * Handles mouse down events.
4324  * @private
4325  * @param {MouseEvent} e Mouse down event
4326  */
4327 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
4328         if (
4329                 this.isVisible() &&
4330                 !$.contains( this.$element[ 0 ], e.target ) &&
4331                 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
4332         ) {
4333                 this.toggle( false );
4334         }
4338  * Bind mouse down listener.
4340  * @private
4341  */
4342 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
4343         // Capture clicks outside popup
4344         this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
4348  * Handles close button click events.
4350  * @private
4351  */
4352 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
4353         if ( this.isVisible() ) {
4354                 this.toggle( false );
4355         }
4359  * Unbind mouse down listener.
4361  * @private
4362  */
4363 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
4364         this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
4368  * Handles key down events.
4370  * @private
4371  * @param {KeyboardEvent} e Key down event
4372  */
4373 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
4374         if (
4375                 e.which === OO.ui.Keys.ESCAPE &&
4376                 this.isVisible()
4377         ) {
4378                 this.toggle( false );
4379                 e.preventDefault();
4380                 e.stopPropagation();
4381         }
4385  * Bind key down listener.
4387  * @private
4388  */
4389 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
4390         this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
4394  * Unbind key down listener.
4396  * @private
4397  */
4398 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
4399         this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
4403  * Show, hide, or toggle the visibility of the anchor.
4405  * @param {boolean} [show] Show anchor, omit to toggle
4406  */
4407 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
4408         show = show === undefined ? !this.anchored : !!show;
4410         if ( this.anchored !== show ) {
4411                 if ( show ) {
4412                         this.$element.addClass( 'oo-ui-popupWidget-anchored' );
4413                 } else {
4414                         this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
4415                 }
4416                 this.anchored = show;
4417         }
4421  * Check if the anchor is visible.
4423  * @return {boolean} Anchor is visible
4424  */
4425 OO.ui.PopupWidget.prototype.hasAnchor = function () {
4426         return this.anchor;
4430  * @inheritdoc
4431  */
4432 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
4433         var change;
4434         show = show === undefined ? !this.isVisible() : !!show;
4436         change = show !== this.isVisible();
4438         // Parent method
4439         OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
4441         if ( change ) {
4442                 if ( show ) {
4443                         if ( this.autoClose ) {
4444                                 this.bindMouseDownListener();
4445                                 this.bindKeyDownListener();
4446                         }
4447                         this.updateDimensions();
4448                         this.toggleClipping( true );
4449                 } else {
4450                         this.toggleClipping( false );
4451                         if ( this.autoClose ) {
4452                                 this.unbindMouseDownListener();
4453                                 this.unbindKeyDownListener();
4454                         }
4455                 }
4456         }
4458         return this;
4462  * Set the size of the popup.
4464  * Changing the size may also change the popup's position depending on the alignment.
4466  * @param {number} width Width in pixels
4467  * @param {number} height Height in pixels
4468  * @param {boolean} [transition=false] Use a smooth transition
4469  * @chainable
4470  */
4471 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
4472         this.width = width;
4473         this.height = height !== undefined ? height : null;
4474         if ( this.isVisible() ) {
4475                 this.updateDimensions( transition );
4476         }
4480  * Update the size and position.
4482  * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
4483  * be called automatically.
4485  * @param {boolean} [transition=false] Use a smooth transition
4486  * @chainable
4487  */
4488 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
4489         var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
4490                 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
4491                 align = this.align,
4492                 widget = this;
4494         if ( !this.$container ) {
4495                 // Lazy-initialize $container if not specified in constructor
4496                 this.$container = $( this.getClosestScrollableElementContainer() );
4497         }
4499         // Set height and width before measuring things, since it might cause our measurements
4500         // to change (e.g. due to scrollbars appearing or disappearing)
4501         this.$popup.css( {
4502                 width: this.width,
4503                 height: this.height !== null ? this.height : 'auto'
4504         } );
4506         // If we are in RTL, we need to flip the alignment, unless it is center
4507         if ( align === 'forwards' || align === 'backwards' ) {
4508                 if ( this.$container.css( 'direction' ) === 'rtl' ) {
4509                         align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
4510                 } else {
4511                         align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
4512                 }
4514         }
4516         // Compute initial popupOffset based on alignment
4517         popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
4519         // Figure out if this will cause the popup to go beyond the edge of the container
4520         originOffset = this.$element.offset().left;
4521         containerLeft = this.$container.offset().left;
4522         containerWidth = this.$container.innerWidth();
4523         containerRight = containerLeft + containerWidth;
4524         popupLeft = popupOffset - this.containerPadding;
4525         popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
4526         overlapLeft = ( originOffset + popupLeft ) - containerLeft;
4527         overlapRight = containerRight - ( originOffset + popupRight );
4529         // Adjust offset to make the popup not go beyond the edge, if needed
4530         if ( overlapRight < 0 ) {
4531                 popupOffset += overlapRight;
4532         } else if ( overlapLeft < 0 ) {
4533                 popupOffset -= overlapLeft;
4534         }
4536         // Adjust offset to avoid anchor being rendered too close to the edge
4537         // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
4538         // TODO: Find a measurement that works for CSS anchors and image anchors
4539         anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
4540         if ( popupOffset + this.width < anchorWidth ) {
4541                 popupOffset = anchorWidth - this.width;
4542         } else if ( -popupOffset < anchorWidth ) {
4543                 popupOffset = -anchorWidth;
4544         }
4546         // Prevent transition from being interrupted
4547         clearTimeout( this.transitionTimeout );
4548         if ( transition ) {
4549                 // Enable transition
4550                 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
4551         }
4553         // Position body relative to anchor
4554         this.$popup.css( 'margin-left', popupOffset );
4556         if ( transition ) {
4557                 // Prevent transitioning after transition is complete
4558                 this.transitionTimeout = setTimeout( function () {
4559                         widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
4560                 }, 200 );
4561         } else {
4562                 // Prevent transitioning immediately
4563                 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
4564         }
4566         // Reevaluate clipping state since we've relocated and resized the popup
4567         this.clip();
4569         return this;
4573  * Set popup alignment
4575  * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4576  *  `backwards` or `forwards`.
4577  */
4578 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
4579         // Validate alignment and transform deprecated values
4580         if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
4581                 this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
4582         } else {
4583                 this.align = 'center';
4584         }
4588  * Get popup alignment
4590  * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4591  *  `backwards` or `forwards`.
4592  */
4593 OO.ui.PopupWidget.prototype.getAlignment = function () {
4594         return this.align;
4598  * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
4599  * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
4600  * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
4601  * See {@link OO.ui.PopupWidget PopupWidget} for an example.
4603  * @abstract
4604  * @class
4606  * @constructor
4607  * @param {Object} [config] Configuration options
4608  * @cfg {Object} [popup] Configuration to pass to popup
4609  * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
4610  */
4611 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
4612         // Configuration initialization
4613         config = config || {};
4615         // Properties
4616         this.popup = new OO.ui.PopupWidget( $.extend(
4617                 { autoClose: true },
4618                 config.popup,
4619                 { $autoCloseIgnore: this.$element }
4620         ) );
4623 /* Methods */
4626  * Get popup.
4628  * @return {OO.ui.PopupWidget} Popup widget
4629  */
4630 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
4631         return this.popup;
4635  * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
4636  * which is used to display additional information or options.
4638  *     @example
4639  *     // Example of a popup button.
4640  *     var popupButton = new OO.ui.PopupButtonWidget( {
4641  *         label: 'Popup button with options',
4642  *         icon: 'menu',
4643  *         popup: {
4644  *             $content: $( '<p>Additional options here.</p>' ),
4645  *             padded: true,
4646  *             align: 'force-left'
4647  *         }
4648  *     } );
4649  *     // Append the button to the DOM.
4650  *     $( 'body' ).append( popupButton.$element );
4652  * @class
4653  * @extends OO.ui.ButtonWidget
4654  * @mixins OO.ui.mixin.PopupElement
4656  * @constructor
4657  * @param {Object} [config] Configuration options
4658  */
4659 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
4660         // Parent constructor
4661         OO.ui.PopupButtonWidget.parent.call( this, config );
4663         // Mixin constructors
4664         OO.ui.mixin.PopupElement.call( this, config );
4666         // Events
4667         this.connect( this, { click: 'onAction' } );
4669         // Initialization
4670         this.$element
4671                 .addClass( 'oo-ui-popupButtonWidget' )
4672                 .attr( 'aria-haspopup', 'true' )
4673                 .append( this.popup.$element );
4676 /* Setup */
4678 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
4679 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
4681 /* Methods */
4684  * Handle the button action being triggered.
4686  * @private
4687  */
4688 OO.ui.PopupButtonWidget.prototype.onAction = function () {
4689         this.popup.toggle();
4693  * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
4695  * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
4697  * @private
4698  * @abstract
4699  * @class
4700  * @mixins OO.ui.mixin.GroupElement
4702  * @constructor
4703  * @param {Object} [config] Configuration options
4704  */
4705 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
4706         // Mixin constructors
4707         OO.ui.mixin.GroupElement.call( this, config );
4710 /* Setup */
4712 OO.mixinClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
4714 /* Methods */
4717  * Set the disabled state of the widget.
4719  * This will also update the disabled state of child widgets.
4721  * @param {boolean} disabled Disable widget
4722  * @chainable
4723  */
4724 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
4725         var i, len;
4727         // Parent method
4728         // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
4729         OO.ui.Widget.prototype.setDisabled.call( this, disabled );
4731         // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
4732         if ( this.items ) {
4733                 for ( i = 0, len = this.items.length; i < len; i++ ) {
4734                         this.items[ i ].updateDisabled();
4735                 }
4736         }
4738         return this;
4742  * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
4744  * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
4745  * allows bidirectional communication.
4747  * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
4749  * @private
4750  * @abstract
4751  * @class
4753  * @constructor
4754  */
4755 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
4756         //
4759 /* Methods */
4762  * Check if widget is disabled.
4764  * Checks parent if present, making disabled state inheritable.
4766  * @return {boolean} Widget is disabled
4767  */
4768 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
4769         return this.disabled ||
4770                 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
4774  * Set group element is in.
4776  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
4777  * @chainable
4778  */
4779 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
4780         // Parent method
4781         // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
4782         OO.ui.Element.prototype.setElementGroup.call( this, group );
4784         // Initialize item disabled states
4785         this.updateDisabled();
4787         return this;
4791  * OptionWidgets are special elements that can be selected and configured with data. The
4792  * data is often unique for each option, but it does not have to be. OptionWidgets are used
4793  * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
4794  * and examples, please see the [OOjs UI documentation on MediaWiki][1].
4796  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4798  * @class
4799  * @extends OO.ui.Widget
4800  * @mixins OO.ui.mixin.ItemWidget
4801  * @mixins OO.ui.mixin.LabelElement
4802  * @mixins OO.ui.mixin.FlaggedElement
4803  * @mixins OO.ui.mixin.AccessKeyedElement
4805  * @constructor
4806  * @param {Object} [config] Configuration options
4807  */
4808 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
4809         // Configuration initialization
4810         config = config || {};
4812         // Parent constructor
4813         OO.ui.OptionWidget.parent.call( this, config );
4815         // Mixin constructors
4816         OO.ui.mixin.ItemWidget.call( this );
4817         OO.ui.mixin.LabelElement.call( this, config );
4818         OO.ui.mixin.FlaggedElement.call( this, config );
4819         OO.ui.mixin.AccessKeyedElement.call( this, config );
4821         // Properties
4822         this.selected = false;
4823         this.highlighted = false;
4824         this.pressed = false;
4826         // Initialization
4827         this.$element
4828                 .data( 'oo-ui-optionWidget', this )
4829                 // Allow programmatic focussing (and by accesskey), but not tabbing
4830                 .attr( 'tabindex', '-1' )
4831                 .attr( 'role', 'option' )
4832                 .attr( 'aria-selected', 'false' )
4833                 .addClass( 'oo-ui-optionWidget' )
4834                 .append( this.$label );
4837 /* Setup */
4839 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
4840 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
4841 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
4842 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
4843 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.AccessKeyedElement );
4845 /* Static Properties */
4847 OO.ui.OptionWidget.static.selectable = true;
4849 OO.ui.OptionWidget.static.highlightable = true;
4851 OO.ui.OptionWidget.static.pressable = true;
4853 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
4855 /* Methods */
4858  * Check if the option can be selected.
4860  * @return {boolean} Item is selectable
4861  */
4862 OO.ui.OptionWidget.prototype.isSelectable = function () {
4863         return this.constructor.static.selectable && !this.isDisabled() && this.isVisible();
4867  * Check if the option can be highlighted. A highlight indicates that the option
4868  * may be selected when a user presses enter or clicks. Disabled items cannot
4869  * be highlighted.
4871  * @return {boolean} Item is highlightable
4872  */
4873 OO.ui.OptionWidget.prototype.isHighlightable = function () {
4874         return this.constructor.static.highlightable && !this.isDisabled() && this.isVisible();
4878  * Check if the option can be pressed. The pressed state occurs when a user mouses
4879  * down on an item, but has not yet let go of the mouse.
4881  * @return {boolean} Item is pressable
4882  */
4883 OO.ui.OptionWidget.prototype.isPressable = function () {
4884         return this.constructor.static.pressable && !this.isDisabled() && this.isVisible();
4888  * Check if the option is selected.
4890  * @return {boolean} Item is selected
4891  */
4892 OO.ui.OptionWidget.prototype.isSelected = function () {
4893         return this.selected;
4897  * Check if the option is highlighted. A highlight indicates that the
4898  * item may be selected when a user presses enter or clicks.
4900  * @return {boolean} Item is highlighted
4901  */
4902 OO.ui.OptionWidget.prototype.isHighlighted = function () {
4903         return this.highlighted;
4907  * Check if the option is pressed. The pressed state occurs when a user mouses
4908  * down on an item, but has not yet let go of the mouse. The item may appear
4909  * selected, but it will not be selected until the user releases the mouse.
4911  * @return {boolean} Item is pressed
4912  */
4913 OO.ui.OptionWidget.prototype.isPressed = function () {
4914         return this.pressed;
4918  * Set the option’s selected state. In general, all modifications to the selection
4919  * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
4920  * method instead of this method.
4922  * @param {boolean} [state=false] Select option
4923  * @chainable
4924  */
4925 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
4926         if ( this.constructor.static.selectable ) {
4927                 this.selected = !!state;
4928                 this.$element
4929                         .toggleClass( 'oo-ui-optionWidget-selected', state )
4930                         .attr( 'aria-selected', state.toString() );
4931                 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
4932                         this.scrollElementIntoView();
4933                 }
4934                 this.updateThemeClasses();
4935         }
4936         return this;
4940  * Set the option’s highlighted state. In general, all programmatic
4941  * modifications to the highlight should be handled by the
4942  * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
4943  * method instead of this method.
4945  * @param {boolean} [state=false] Highlight option
4946  * @chainable
4947  */
4948 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
4949         if ( this.constructor.static.highlightable ) {
4950                 this.highlighted = !!state;
4951                 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
4952                 this.updateThemeClasses();
4953         }
4954         return this;
4958  * Set the option’s pressed state. In general, all
4959  * programmatic modifications to the pressed state should be handled by the
4960  * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
4961  * method instead of this method.
4963  * @param {boolean} [state=false] Press option
4964  * @chainable
4965  */
4966 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
4967         if ( this.constructor.static.pressable ) {
4968                 this.pressed = !!state;
4969                 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
4970                 this.updateThemeClasses();
4971         }
4972         return this;
4976  * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
4977  * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
4978  * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
4979  * menu selects}.
4981  * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
4982  * information, please see the [OOjs UI documentation on MediaWiki][1].
4984  *     @example
4985  *     // Example of a select widget with three options
4986  *     var select = new OO.ui.SelectWidget( {
4987  *         items: [
4988  *             new OO.ui.OptionWidget( {
4989  *                 data: 'a',
4990  *                 label: 'Option One',
4991  *             } ),
4992  *             new OO.ui.OptionWidget( {
4993  *                 data: 'b',
4994  *                 label: 'Option Two',
4995  *             } ),
4996  *             new OO.ui.OptionWidget( {
4997  *                 data: 'c',
4998  *                 label: 'Option Three',
4999  *             } )
5000  *         ]
5001  *     } );
5002  *     $( 'body' ).append( select.$element );
5004  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5006  * @abstract
5007  * @class
5008  * @extends OO.ui.Widget
5009  * @mixins OO.ui.mixin.GroupWidget
5011  * @constructor
5012  * @param {Object} [config] Configuration options
5013  * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
5014  *  Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
5015  *  the [OOjs UI documentation on MediaWiki] [2] for examples.
5016  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5017  */
5018 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
5019         // Configuration initialization
5020         config = config || {};
5022         // Parent constructor
5023         OO.ui.SelectWidget.parent.call( this, config );
5025         // Mixin constructors
5026         OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
5028         // Properties
5029         this.pressed = false;
5030         this.selecting = null;
5031         this.onMouseUpHandler = this.onMouseUp.bind( this );
5032         this.onMouseMoveHandler = this.onMouseMove.bind( this );
5033         this.onKeyDownHandler = this.onKeyDown.bind( this );
5034         this.onKeyPressHandler = this.onKeyPress.bind( this );
5035         this.keyPressBuffer = '';
5036         this.keyPressBufferTimer = null;
5037         this.blockMouseOverEvents = 0;
5039         // Events
5040         this.connect( this, {
5041                 toggle: 'onToggle'
5042         } );
5043         this.$element.on( {
5044                 focusin: this.onFocus.bind( this ),
5045                 mousedown: this.onMouseDown.bind( this ),
5046                 mouseover: this.onMouseOver.bind( this ),
5047                 mouseleave: this.onMouseLeave.bind( this )
5048         } );
5050         // Initialization
5051         this.$element
5052                 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
5053                 .attr( 'role', 'listbox' );
5054         if ( Array.isArray( config.items ) ) {
5055                 this.addItems( config.items );
5056         }
5059 /* Setup */
5061 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
5062 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
5064 /* Events */
5067  * @event highlight
5069  * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
5071  * @param {OO.ui.OptionWidget|null} item Highlighted item
5072  */
5075  * @event press
5077  * A `press` event is emitted when the #pressItem method is used to programmatically modify the
5078  * pressed state of an option.
5080  * @param {OO.ui.OptionWidget|null} item Pressed item
5081  */
5084  * @event select
5086  * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
5088  * @param {OO.ui.OptionWidget|null} item Selected item
5089  */
5092  * @event choose
5093  * A `choose` event is emitted when an item is chosen with the #chooseItem method.
5094  * @param {OO.ui.OptionWidget} item Chosen item
5095  */
5098  * @event add
5100  * An `add` event is emitted when options are added to the select with the #addItems method.
5102  * @param {OO.ui.OptionWidget[]} items Added items
5103  * @param {number} index Index of insertion point
5104  */
5107  * @event remove
5109  * A `remove` event is emitted when options are removed from the select with the #clearItems
5110  * or #removeItems methods.
5112  * @param {OO.ui.OptionWidget[]} items Removed items
5113  */
5115 /* Methods */
5118  * Handle focus events
5120  * @private
5121  * @param {jQuery.Event} event
5122  */
5123 OO.ui.SelectWidget.prototype.onFocus = function ( event ) {
5124         var item;
5125         if ( event.target === this.$element[ 0 ] ) {
5126                 // This widget was focussed, e.g. by the user tabbing to it.
5127                 // The styles for focus state depend on one of the items being selected.
5128                 if ( !this.getSelectedItem() ) {
5129                         item = this.getFirstSelectableItem();
5130                 }
5131         } else {
5132                 // One of the options got focussed (and the event bubbled up here).
5133                 // They can't be tabbed to, but they can be activated using accesskeys.
5134                 item = this.getTargetItem( event );
5135         }
5137         if ( item ) {
5138                 if ( item.constructor.static.highlightable ) {
5139                         this.highlightItem( item );
5140                 } else {
5141                         this.selectItem( item );
5142                 }
5143         }
5145         if ( event.target !== this.$element[ 0 ] ) {
5146                 this.$element.focus();
5147         }
5151  * Handle mouse down events.
5153  * @private
5154  * @param {jQuery.Event} e Mouse down event
5155  */
5156 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
5157         var item;
5159         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
5160                 this.togglePressed( true );
5161                 item = this.getTargetItem( e );
5162                 if ( item && item.isSelectable() ) {
5163                         this.pressItem( item );
5164                         this.selecting = item;
5165                         this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
5166                         this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler, true );
5167                 }
5168         }
5169         return false;
5173  * Handle mouse up events.
5175  * @private
5176  * @param {MouseEvent} e Mouse up event
5177  */
5178 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
5179         var item;
5181         this.togglePressed( false );
5182         if ( !this.selecting ) {
5183                 item = this.getTargetItem( e );
5184                 if ( item && item.isSelectable() ) {
5185                         this.selecting = item;
5186                 }
5187         }
5188         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT && this.selecting ) {
5189                 this.pressItem( null );
5190                 this.chooseItem( this.selecting );
5191                 this.selecting = null;
5192         }
5194         this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
5195         this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler, true );
5197         return false;
5201  * Handle mouse move events.
5203  * @private
5204  * @param {MouseEvent} e Mouse move event
5205  */
5206 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
5207         var item;
5209         if ( !this.isDisabled() && this.pressed ) {
5210                 item = this.getTargetItem( e );
5211                 if ( item && item !== this.selecting && item.isSelectable() ) {
5212                         this.pressItem( item );
5213                         this.selecting = item;
5214                 }
5215         }
5219  * Handle mouse over events.
5221  * @private
5222  * @param {jQuery.Event} e Mouse over event
5223  */
5224 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
5225         var item;
5226         if ( this.blockMouseOverEvents ) {
5227                 return;
5228         }
5229         if ( !this.isDisabled() ) {
5230                 item = this.getTargetItem( e );
5231                 this.highlightItem( item && item.isHighlightable() ? item : null );
5232         }
5233         return false;
5237  * Handle mouse leave events.
5239  * @private
5240  * @param {jQuery.Event} e Mouse over event
5241  */
5242 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
5243         if ( !this.isDisabled() ) {
5244                 this.highlightItem( null );
5245         }
5246         return false;
5250  * Handle key down events.
5252  * @protected
5253  * @param {KeyboardEvent} e Key down event
5254  */
5255 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
5256         var nextItem,
5257                 handled = false,
5258                 currentItem = this.getHighlightedItem() || this.getSelectedItem();
5260         if ( !this.isDisabled() && this.isVisible() ) {
5261                 switch ( e.keyCode ) {
5262                         case OO.ui.Keys.ENTER:
5263                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
5264                                         // Was only highlighted, now let's select it. No-op if already selected.
5265                                         this.chooseItem( currentItem );
5266                                         handled = true;
5267                                 }
5268                                 break;
5269                         case OO.ui.Keys.UP:
5270                         case OO.ui.Keys.LEFT:
5271                                 this.clearKeyPressBuffer();
5272                                 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
5273                                 handled = true;
5274                                 break;
5275                         case OO.ui.Keys.DOWN:
5276                         case OO.ui.Keys.RIGHT:
5277                                 this.clearKeyPressBuffer();
5278                                 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
5279                                 handled = true;
5280                                 break;
5281                         case OO.ui.Keys.ESCAPE:
5282                         case OO.ui.Keys.TAB:
5283                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
5284                                         currentItem.setHighlighted( false );
5285                                 }
5286                                 this.unbindKeyDownListener();
5287                                 this.unbindKeyPressListener();
5288                                 // Don't prevent tabbing away / defocusing
5289                                 handled = false;
5290                                 break;
5291                 }
5293                 if ( nextItem ) {
5294                         if ( nextItem.constructor.static.highlightable ) {
5295                                 this.highlightItem( nextItem );
5296                         } else {
5297                                 this.chooseItem( nextItem );
5298                         }
5299                         this.scrollItemIntoView( nextItem );
5300                 }
5302                 if ( handled ) {
5303                         e.preventDefault();
5304                         e.stopPropagation();
5305                 }
5306         }
5310  * Bind key down listener.
5312  * @protected
5313  */
5314 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
5315         this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
5319  * Unbind key down listener.
5321  * @protected
5322  */
5323 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
5324         this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
5328  * Scroll item into view, preventing spurious mouse highlight actions from happening.
5330  * @param {OO.ui.OptionWidget} item Item to scroll into view
5331  */
5332 OO.ui.SelectWidget.prototype.scrollItemIntoView = function ( item ) {
5333         var widget = this;
5334         // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
5335         // and around 100-150 ms after it is finished.
5336         this.blockMouseOverEvents++;
5337         item.scrollElementIntoView().done( function () {
5338                 setTimeout( function () {
5339                         widget.blockMouseOverEvents--;
5340                 }, 200 );
5341         } );
5345  * Clear the key-press buffer
5347  * @protected
5348  */
5349 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
5350         if ( this.keyPressBufferTimer ) {
5351                 clearTimeout( this.keyPressBufferTimer );
5352                 this.keyPressBufferTimer = null;
5353         }
5354         this.keyPressBuffer = '';
5358  * Handle key press events.
5360  * @protected
5361  * @param {KeyboardEvent} e Key press event
5362  */
5363 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
5364         var c, filter, item;
5366         if ( !e.charCode ) {
5367                 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
5368                         this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
5369                         return false;
5370                 }
5371                 return;
5372         }
5373         if ( String.fromCodePoint ) {
5374                 c = String.fromCodePoint( e.charCode );
5375         } else {
5376                 c = String.fromCharCode( e.charCode );
5377         }
5379         if ( this.keyPressBufferTimer ) {
5380                 clearTimeout( this.keyPressBufferTimer );
5381         }
5382         this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
5384         item = this.getHighlightedItem() || this.getSelectedItem();
5386         if ( this.keyPressBuffer === c ) {
5387                 // Common (if weird) special case: typing "xxxx" will cycle through all
5388                 // the items beginning with "x".
5389                 if ( item ) {
5390                         item = this.getRelativeSelectableItem( item, 1 );
5391                 }
5392         } else {
5393                 this.keyPressBuffer += c;
5394         }
5396         filter = this.getItemMatcher( this.keyPressBuffer, false );
5397         if ( !item || !filter( item ) ) {
5398                 item = this.getRelativeSelectableItem( item, 1, filter );
5399         }
5400         if ( item ) {
5401                 if ( this.isVisible() && item.constructor.static.highlightable ) {
5402                         this.highlightItem( item );
5403                 } else {
5404                         this.chooseItem( item );
5405                 }
5406                 this.scrollItemIntoView( item );
5407         }
5409         e.preventDefault();
5410         e.stopPropagation();
5414  * Get a matcher for the specific string
5416  * @protected
5417  * @param {string} s String to match against items
5418  * @param {boolean} [exact=false] Only accept exact matches
5419  * @return {Function} function ( OO.ui.OptionItem ) => boolean
5420  */
5421 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
5422         var re;
5424         if ( s.normalize ) {
5425                 s = s.normalize();
5426         }
5427         s = exact ? s.trim() : s.replace( /^\s+/, '' );
5428         re = '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
5429         if ( exact ) {
5430                 re += '\\s*$';
5431         }
5432         re = new RegExp( re, 'i' );
5433         return function ( item ) {
5434                 var l = item.getLabel();
5435                 if ( typeof l !== 'string' ) {
5436                         l = item.$label.text();
5437                 }
5438                 if ( l.normalize ) {
5439                         l = l.normalize();
5440                 }
5441                 return re.test( l );
5442         };
5446  * Bind key press listener.
5448  * @protected
5449  */
5450 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
5451         this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
5455  * Unbind key down listener.
5457  * If you override this, be sure to call this.clearKeyPressBuffer() from your
5458  * implementation.
5460  * @protected
5461  */
5462 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
5463         this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
5464         this.clearKeyPressBuffer();
5468  * Visibility change handler
5470  * @protected
5471  * @param {boolean} visible
5472  */
5473 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
5474         if ( !visible ) {
5475                 this.clearKeyPressBuffer();
5476         }
5480  * Get the closest item to a jQuery.Event.
5482  * @private
5483  * @param {jQuery.Event} e
5484  * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5485  */
5486 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
5487         return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
5491  * Get selected item.
5493  * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5494  */
5495 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
5496         var i, len;
5498         for ( i = 0, len = this.items.length; i < len; i++ ) {
5499                 if ( this.items[ i ].isSelected() ) {
5500                         return this.items[ i ];
5501                 }
5502         }
5503         return null;
5507  * Get highlighted item.
5509  * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5510  */
5511 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
5512         var i, len;
5514         for ( i = 0, len = this.items.length; i < len; i++ ) {
5515                 if ( this.items[ i ].isHighlighted() ) {
5516                         return this.items[ i ];
5517                 }
5518         }
5519         return null;
5523  * Toggle pressed state.
5525  * Press is a state that occurs when a user mouses down on an item, but
5526  * has not yet let go of the mouse. The item may appear selected, but it will not be selected
5527  * until the user releases the mouse.
5529  * @param {boolean} pressed An option is being pressed
5530  */
5531 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
5532         if ( pressed === undefined ) {
5533                 pressed = !this.pressed;
5534         }
5535         if ( pressed !== this.pressed ) {
5536                 this.$element
5537                         .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
5538                         .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
5539                 this.pressed = pressed;
5540         }
5544  * Highlight an option. If the `item` param is omitted, no options will be highlighted
5545  * and any existing highlight will be removed. The highlight is mutually exclusive.
5547  * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
5548  * @fires highlight
5549  * @chainable
5550  */
5551 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
5552         var i, len, highlighted,
5553                 changed = false;
5555         for ( i = 0, len = this.items.length; i < len; i++ ) {
5556                 highlighted = this.items[ i ] === item;
5557                 if ( this.items[ i ].isHighlighted() !== highlighted ) {
5558                         this.items[ i ].setHighlighted( highlighted );
5559                         changed = true;
5560                 }
5561         }
5562         if ( changed ) {
5563                 this.emit( 'highlight', item );
5564         }
5566         return this;
5570  * Fetch an item by its label.
5572  * @param {string} label Label of the item to select.
5573  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5574  * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
5575  */
5576 OO.ui.SelectWidget.prototype.getItemFromLabel = function ( label, prefix ) {
5577         var i, item, found,
5578                 len = this.items.length,
5579                 filter = this.getItemMatcher( label, true );
5581         for ( i = 0; i < len; i++ ) {
5582                 item = this.items[ i ];
5583                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
5584                         return item;
5585                 }
5586         }
5588         if ( prefix ) {
5589                 found = null;
5590                 filter = this.getItemMatcher( label, false );
5591                 for ( i = 0; i < len; i++ ) {
5592                         item = this.items[ i ];
5593                         if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
5594                                 if ( found ) {
5595                                         return null;
5596                                 }
5597                                 found = item;
5598                         }
5599                 }
5600                 if ( found ) {
5601                         return found;
5602                 }
5603         }
5605         return null;
5609  * Programmatically select an option by its label. If the item does not exist,
5610  * all options will be deselected.
5612  * @param {string} [label] Label of the item to select.
5613  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5614  * @fires select
5615  * @chainable
5616  */
5617 OO.ui.SelectWidget.prototype.selectItemByLabel = function ( label, prefix ) {
5618         var itemFromLabel = this.getItemFromLabel( label, !!prefix );
5619         if ( label === undefined || !itemFromLabel ) {
5620                 return this.selectItem();
5621         }
5622         return this.selectItem( itemFromLabel );
5626  * Programmatically select an option by its data. If the `data` parameter is omitted,
5627  * or if the item does not exist, all options will be deselected.
5629  * @param {Object|string} [data] Value of the item to select, omit to deselect all
5630  * @fires select
5631  * @chainable
5632  */
5633 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
5634         var itemFromData = this.getItemFromData( data );
5635         if ( data === undefined || !itemFromData ) {
5636                 return this.selectItem();
5637         }
5638         return this.selectItem( itemFromData );
5642  * Programmatically select an option by its reference. If the `item` parameter is omitted,
5643  * all options will be deselected.
5645  * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
5646  * @fires select
5647  * @chainable
5648  */
5649 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
5650         var i, len, selected,
5651                 changed = false;
5653         for ( i = 0, len = this.items.length; i < len; i++ ) {
5654                 selected = this.items[ i ] === item;
5655                 if ( this.items[ i ].isSelected() !== selected ) {
5656                         this.items[ i ].setSelected( selected );
5657                         changed = true;
5658                 }
5659         }
5660         if ( changed ) {
5661                 this.emit( 'select', item );
5662         }
5664         return this;
5668  * Press an item.
5670  * Press is a state that occurs when a user mouses down on an item, but has not
5671  * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
5672  * releases the mouse.
5674  * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
5675  * @fires press
5676  * @chainable
5677  */
5678 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
5679         var i, len, pressed,
5680                 changed = false;
5682         for ( i = 0, len = this.items.length; i < len; i++ ) {
5683                 pressed = this.items[ i ] === item;
5684                 if ( this.items[ i ].isPressed() !== pressed ) {
5685                         this.items[ i ].setPressed( pressed );
5686                         changed = true;
5687                 }
5688         }
5689         if ( changed ) {
5690                 this.emit( 'press', item );
5691         }
5693         return this;
5697  * Choose an item.
5699  * Note that ‘choose’ should never be modified programmatically. A user can choose
5700  * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
5701  * use the #selectItem method.
5703  * This method is identical to #selectItem, but may vary in subclasses that take additional action
5704  * when users choose an item with the keyboard or mouse.
5706  * @param {OO.ui.OptionWidget} item Item to choose
5707  * @fires choose
5708  * @chainable
5709  */
5710 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
5711         if ( item ) {
5712                 this.selectItem( item );
5713                 this.emit( 'choose', item );
5714         }
5716         return this;
5720  * Get an option by its position relative to the specified item (or to the start of the option array,
5721  * if item is `null`). The direction in which to search through the option array is specified with a
5722  * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
5723  * `null` if there are no options in the array.
5725  * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
5726  * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
5727  * @param {Function} [filter] Only consider items for which this function returns
5728  *  true. Function takes an OO.ui.OptionWidget and returns a boolean.
5729  * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
5730  */
5731 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
5732         var currentIndex, nextIndex, i,
5733                 increase = direction > 0 ? 1 : -1,
5734                 len = this.items.length;
5736         if ( item instanceof OO.ui.OptionWidget ) {
5737                 currentIndex = this.items.indexOf( item );
5738                 nextIndex = ( currentIndex + increase + len ) % len;
5739         } else {
5740                 // If no item is selected and moving forward, start at the beginning.
5741                 // If moving backward, start at the end.
5742                 nextIndex = direction > 0 ? 0 : len - 1;
5743         }
5745         for ( i = 0; i < len; i++ ) {
5746                 item = this.items[ nextIndex ];
5747                 if (
5748                         item instanceof OO.ui.OptionWidget && item.isSelectable() &&
5749                         ( !filter || filter( item ) )
5750                 ) {
5751                         return item;
5752                 }
5753                 nextIndex = ( nextIndex + increase + len ) % len;
5754         }
5755         return null;
5759  * Get the next selectable item or `null` if there are no selectable items.
5760  * Disabled options and menu-section markers and breaks are not selectable.
5762  * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
5763  */
5764 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
5765         return this.getRelativeSelectableItem( null, 1 );
5769  * Add an array of options to the select. Optionally, an index number can be used to
5770  * specify an insertion point.
5772  * @param {OO.ui.OptionWidget[]} items Items to add
5773  * @param {number} [index] Index to insert items after
5774  * @fires add
5775  * @chainable
5776  */
5777 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
5778         // Mixin method
5779         OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
5781         // Always provide an index, even if it was omitted
5782         this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
5784         return this;
5788  * Remove the specified array of options from the select. Options will be detached
5789  * from the DOM, not removed, so they can be reused later. To remove all options from
5790  * the select, you may wish to use the #clearItems method instead.
5792  * @param {OO.ui.OptionWidget[]} items Items to remove
5793  * @fires remove
5794  * @chainable
5795  */
5796 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
5797         var i, len, item;
5799         // Deselect items being removed
5800         for ( i = 0, len = items.length; i < len; i++ ) {
5801                 item = items[ i ];
5802                 if ( item.isSelected() ) {
5803                         this.selectItem( null );
5804                 }
5805         }
5807         // Mixin method
5808         OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
5810         this.emit( 'remove', items );
5812         return this;
5816  * Clear all options from the select. Options will be detached from the DOM, not removed,
5817  * so that they can be reused later. To remove a subset of options from the select, use
5818  * the #removeItems method.
5820  * @fires remove
5821  * @chainable
5822  */
5823 OO.ui.SelectWidget.prototype.clearItems = function () {
5824         var items = this.items.slice();
5826         // Mixin method
5827         OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
5829         // Clear selection
5830         this.selectItem( null );
5832         this.emit( 'remove', items );
5834         return this;
5838  * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
5839  * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
5840  * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
5841  * options. For more information about options and selects, please see the
5842  * [OOjs UI documentation on MediaWiki][1].
5844  *     @example
5845  *     // Decorated options in a select widget
5846  *     var select = new OO.ui.SelectWidget( {
5847  *         items: [
5848  *             new OO.ui.DecoratedOptionWidget( {
5849  *                 data: 'a',
5850  *                 label: 'Option with icon',
5851  *                 icon: 'help'
5852  *             } ),
5853  *             new OO.ui.DecoratedOptionWidget( {
5854  *                 data: 'b',
5855  *                 label: 'Option with indicator',
5856  *                 indicator: 'next'
5857  *             } )
5858  *         ]
5859  *     } );
5860  *     $( 'body' ).append( select.$element );
5862  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5864  * @class
5865  * @extends OO.ui.OptionWidget
5866  * @mixins OO.ui.mixin.IconElement
5867  * @mixins OO.ui.mixin.IndicatorElement
5869  * @constructor
5870  * @param {Object} [config] Configuration options
5871  */
5872 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
5873         // Parent constructor
5874         OO.ui.DecoratedOptionWidget.parent.call( this, config );
5876         // Mixin constructors
5877         OO.ui.mixin.IconElement.call( this, config );
5878         OO.ui.mixin.IndicatorElement.call( this, config );
5880         // Initialization
5881         this.$element
5882                 .addClass( 'oo-ui-decoratedOptionWidget' )
5883                 .prepend( this.$icon )
5884                 .append( this.$indicator );
5887 /* Setup */
5889 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
5890 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
5891 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
5894  * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
5895  * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
5896  * the [OOjs UI documentation on MediaWiki] [1] for more information.
5898  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
5900  * @class
5901  * @extends OO.ui.DecoratedOptionWidget
5903  * @constructor
5904  * @param {Object} [config] Configuration options
5905  */
5906 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
5907         // Configuration initialization
5908         config = $.extend( { icon: 'check' }, config );
5910         // Parent constructor
5911         OO.ui.MenuOptionWidget.parent.call( this, config );
5913         // Initialization
5914         this.$element
5915                 .attr( 'role', 'menuitem' )
5916                 .addClass( 'oo-ui-menuOptionWidget' );
5919 /* Setup */
5921 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
5923 /* Static Properties */
5925 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
5928  * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
5929  * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
5931  *     @example
5932  *     var myDropdown = new OO.ui.DropdownWidget( {
5933  *         menu: {
5934  *             items: [
5935  *                 new OO.ui.MenuSectionOptionWidget( {
5936  *                     label: 'Dogs'
5937  *                 } ),
5938  *                 new OO.ui.MenuOptionWidget( {
5939  *                     data: 'corgi',
5940  *                     label: 'Welsh Corgi'
5941  *                 } ),
5942  *                 new OO.ui.MenuOptionWidget( {
5943  *                     data: 'poodle',
5944  *                     label: 'Standard Poodle'
5945  *                 } ),
5946  *                 new OO.ui.MenuSectionOptionWidget( {
5947  *                     label: 'Cats'
5948  *                 } ),
5949  *                 new OO.ui.MenuOptionWidget( {
5950  *                     data: 'lion',
5951  *                     label: 'Lion'
5952  *                 } )
5953  *             ]
5954  *         }
5955  *     } );
5956  *     $( 'body' ).append( myDropdown.$element );
5958  * @class
5959  * @extends OO.ui.DecoratedOptionWidget
5961  * @constructor
5962  * @param {Object} [config] Configuration options
5963  */
5964 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
5965         // Parent constructor
5966         OO.ui.MenuSectionOptionWidget.parent.call( this, config );
5968         // Initialization
5969         this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
5972 /* Setup */
5974 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
5976 /* Static Properties */
5978 OO.ui.MenuSectionOptionWidget.static.selectable = false;
5980 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
5983  * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
5984  * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
5985  * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
5986  * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
5987  * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
5988  * and customized to be opened, closed, and displayed as needed.
5990  * By default, menus are clipped to the visible viewport and are not visible when a user presses the
5991  * mouse outside the menu.
5993  * Menus also have support for keyboard interaction:
5995  * - Enter/Return key: choose and select a menu option
5996  * - Up-arrow key: highlight the previous menu option
5997  * - Down-arrow key: highlight the next menu option
5998  * - Esc key: hide the menu
6000  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6001  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6003  * @class
6004  * @extends OO.ui.SelectWidget
6005  * @mixins OO.ui.mixin.ClippableElement
6007  * @constructor
6008  * @param {Object} [config] Configuration options
6009  * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
6010  *  the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
6011  *  and {@link OO.ui.mixin.LookupElement LookupElement}
6012  * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
6013  *  the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
6014  * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
6015  *  anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
6016  *  that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
6017  *  that button, unless the button (or its parent widget) is passed in here.
6018  * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
6019  * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
6020  */
6021 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
6022         // Configuration initialization
6023         config = config || {};
6025         // Parent constructor
6026         OO.ui.MenuSelectWidget.parent.call( this, config );
6028         // Mixin constructors
6029         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
6031         // Properties
6032         this.autoHide = config.autoHide === undefined || !!config.autoHide;
6033         this.filterFromInput = !!config.filterFromInput;
6034         this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
6035         this.$widget = config.widget ? config.widget.$element : null;
6036         this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
6037         this.onInputEditHandler = OO.ui.debounce( this.updateItemVisibility.bind( this ), 100 );
6039         // Initialization
6040         this.$element
6041                 .addClass( 'oo-ui-menuSelectWidget' )
6042                 .attr( 'role', 'menu' );
6044         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
6045         // that reference properties not initialized at that time of parent class construction
6046         // TODO: Find a better way to handle post-constructor setup
6047         this.visible = false;
6048         this.$element.addClass( 'oo-ui-element-hidden' );
6051 /* Setup */
6053 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
6054 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
6056 /* Methods */
6059  * Handles document mouse down events.
6061  * @protected
6062  * @param {MouseEvent} e Mouse down event
6063  */
6064 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
6065         if (
6066                 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
6067                 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
6068         ) {
6069                 this.toggle( false );
6070         }
6074  * @inheritdoc
6075  */
6076 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
6077         var currentItem = this.getHighlightedItem() || this.getSelectedItem();
6079         if ( !this.isDisabled() && this.isVisible() ) {
6080                 switch ( e.keyCode ) {
6081                         case OO.ui.Keys.LEFT:
6082                         case OO.ui.Keys.RIGHT:
6083                                 // Do nothing if a text field is associated, arrow keys will be handled natively
6084                                 if ( !this.$input ) {
6085                                         OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
6086                                 }
6087                                 break;
6088                         case OO.ui.Keys.ESCAPE:
6089                         case OO.ui.Keys.TAB:
6090                                 if ( currentItem ) {
6091                                         currentItem.setHighlighted( false );
6092                                 }
6093                                 this.toggle( false );
6094                                 // Don't prevent tabbing away, prevent defocusing
6095                                 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
6096                                         e.preventDefault();
6097                                         e.stopPropagation();
6098                                 }
6099                                 break;
6100                         default:
6101                                 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
6102                                 return;
6103                 }
6104         }
6108  * Update menu item visibility after input changes.
6110  * @protected
6111  */
6112 OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
6113         var i, item,
6114                 len = this.items.length,
6115                 showAll = !this.isVisible(),
6116                 filter = showAll ? null : this.getItemMatcher( this.$input.val() );
6118         for ( i = 0; i < len; i++ ) {
6119                 item = this.items[ i ];
6120                 if ( item instanceof OO.ui.OptionWidget ) {
6121                         item.toggle( showAll || filter( item ) );
6122                 }
6123         }
6125         // Reevaluate clipping
6126         this.clip();
6130  * @inheritdoc
6131  */
6132 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
6133         if ( this.$input ) {
6134                 this.$input.on( 'keydown', this.onKeyDownHandler );
6135         } else {
6136                 OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
6137         }
6141  * @inheritdoc
6142  */
6143 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
6144         if ( this.$input ) {
6145                 this.$input.off( 'keydown', this.onKeyDownHandler );
6146         } else {
6147                 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
6148         }
6152  * @inheritdoc
6153  */
6154 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
6155         if ( this.$input ) {
6156                 if ( this.filterFromInput ) {
6157                         this.$input.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
6158                 }
6159         } else {
6160                 OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
6161         }
6165  * @inheritdoc
6166  */
6167 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
6168         if ( this.$input ) {
6169                 if ( this.filterFromInput ) {
6170                         this.$input.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
6171                         this.updateItemVisibility();
6172                 }
6173         } else {
6174                 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
6175         }
6179  * Choose an item.
6181  * When a user chooses an item, the menu is closed.
6183  * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
6184  * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
6186  * @param {OO.ui.OptionWidget} item Item to choose
6187  * @chainable
6188  */
6189 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
6190         OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
6191         this.toggle( false );
6192         return this;
6196  * @inheritdoc
6197  */
6198 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
6199         // Parent method
6200         OO.ui.MenuSelectWidget.parent.prototype.addItems.call( this, items, index );
6202         // Reevaluate clipping
6203         this.clip();
6205         return this;
6209  * @inheritdoc
6210  */
6211 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
6212         // Parent method
6213         OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
6215         // Reevaluate clipping
6216         this.clip();
6218         return this;
6222  * @inheritdoc
6223  */
6224 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
6225         // Parent method
6226         OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
6228         // Reevaluate clipping
6229         this.clip();
6231         return this;
6235  * @inheritdoc
6236  */
6237 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
6238         var change;
6240         visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
6241         change = visible !== this.isVisible();
6243         // Parent method
6244         OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
6246         if ( change ) {
6247                 if ( visible ) {
6248                         this.bindKeyDownListener();
6249                         this.bindKeyPressListener();
6251                         this.toggleClipping( true );
6253                         if ( this.getSelectedItem() ) {
6254                                 this.getSelectedItem().scrollElementIntoView( { duration: 0 } );
6255                         }
6257                         // Auto-hide
6258                         if ( this.autoHide ) {
6259                                 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
6260                         }
6261                 } else {
6262                         this.unbindKeyDownListener();
6263                         this.unbindKeyPressListener();
6264                         this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
6265                         this.toggleClipping( false );
6266                 }
6267         }
6269         return this;
6273  * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
6274  * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
6275  * users can interact with it.
6277  * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6278  * OO.ui.DropdownInputWidget instead.
6280  *     @example
6281  *     // Example: A DropdownWidget with a menu that contains three options
6282  *     var dropDown = new OO.ui.DropdownWidget( {
6283  *         label: 'Dropdown menu: Select a menu option',
6284  *         menu: {
6285  *             items: [
6286  *                 new OO.ui.MenuOptionWidget( {
6287  *                     data: 'a',
6288  *                     label: 'First'
6289  *                 } ),
6290  *                 new OO.ui.MenuOptionWidget( {
6291  *                     data: 'b',
6292  *                     label: 'Second'
6293  *                 } ),
6294  *                 new OO.ui.MenuOptionWidget( {
6295  *                     data: 'c',
6296  *                     label: 'Third'
6297  *                 } )
6298  *             ]
6299  *         }
6300  *     } );
6302  *     $( 'body' ).append( dropDown.$element );
6304  *     dropDown.getMenu().selectItemByData( 'b' );
6306  *     dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
6308  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
6310  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6312  * @class
6313  * @extends OO.ui.Widget
6314  * @mixins OO.ui.mixin.IconElement
6315  * @mixins OO.ui.mixin.IndicatorElement
6316  * @mixins OO.ui.mixin.LabelElement
6317  * @mixins OO.ui.mixin.TitledElement
6318  * @mixins OO.ui.mixin.TabIndexedElement
6320  * @constructor
6321  * @param {Object} [config] Configuration options
6322  * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
6323  * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
6324  *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6325  *  containing `<div>` and has a larger area. By default, the menu uses relative positioning.
6326  */
6327 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
6328         // Configuration initialization
6329         config = $.extend( { indicator: 'down' }, config );
6331         // Parent constructor
6332         OO.ui.DropdownWidget.parent.call( this, config );
6334         // Properties (must be set before TabIndexedElement constructor call)
6335         this.$handle = this.$( '<span>' );
6336         this.$overlay = config.$overlay || this.$element;
6338         // Mixin constructors
6339         OO.ui.mixin.IconElement.call( this, config );
6340         OO.ui.mixin.IndicatorElement.call( this, config );
6341         OO.ui.mixin.LabelElement.call( this, config );
6342         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
6343         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
6345         // Properties
6346         this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend( {
6347                 widget: this,
6348                 $container: this.$element
6349         }, config.menu ) );
6351         // Events
6352         this.$handle.on( {
6353                 click: this.onClick.bind( this ),
6354                 keydown: this.onKeyDown.bind( this ),
6355                 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
6356                 keypress: this.menu.onKeyPressHandler,
6357                 blur: this.menu.clearKeyPressBuffer.bind( this.menu )
6358         } );
6359         this.menu.connect( this, { select: 'onMenuSelect' } );
6361         // Initialization
6362         this.$handle
6363                 .addClass( 'oo-ui-dropdownWidget-handle' )
6364                 .append( this.$icon, this.$label, this.$indicator );
6365         this.$element
6366                 .addClass( 'oo-ui-dropdownWidget' )
6367                 .append( this.$handle );
6368         this.$overlay.append( this.menu.$element );
6371 /* Setup */
6373 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
6374 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
6375 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
6376 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
6377 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
6378 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
6380 /* Methods */
6383  * Get the menu.
6385  * @return {OO.ui.MenuSelectWidget} Menu of widget
6386  */
6387 OO.ui.DropdownWidget.prototype.getMenu = function () {
6388         return this.menu;
6392  * Handles menu select events.
6394  * @private
6395  * @param {OO.ui.MenuOptionWidget} item Selected menu item
6396  */
6397 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
6398         var selectedLabel;
6400         if ( !item ) {
6401                 this.setLabel( null );
6402                 return;
6403         }
6405         selectedLabel = item.getLabel();
6407         // If the label is a DOM element, clone it, because setLabel will append() it
6408         if ( selectedLabel instanceof jQuery ) {
6409                 selectedLabel = selectedLabel.clone();
6410         }
6412         this.setLabel( selectedLabel );
6416  * Handle mouse click events.
6418  * @private
6419  * @param {jQuery.Event} e Mouse click event
6420  */
6421 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
6422         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
6423                 this.menu.toggle();
6424         }
6425         return false;
6429  * Handle key down events.
6431  * @private
6432  * @param {jQuery.Event} e Key down event
6433  */
6434 OO.ui.DropdownWidget.prototype.onKeyDown = function ( e ) {
6435         if (
6436                 !this.isDisabled() &&
6437                 (
6438                         e.which === OO.ui.Keys.ENTER ||
6439                         (
6440                                 !this.menu.isVisible() &&
6441                                 (
6442                                         e.which === OO.ui.Keys.SPACE ||
6443                                         e.which === OO.ui.Keys.UP ||
6444                                         e.which === OO.ui.Keys.DOWN
6445                                 )
6446                         )
6447                 )
6448         ) {
6449                 this.menu.toggle();
6450                 return false;
6451         }
6455  * RadioOptionWidget is an option widget that looks like a radio button.
6456  * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
6457  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6459  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6461  * @class
6462  * @extends OO.ui.OptionWidget
6464  * @constructor
6465  * @param {Object} [config] Configuration options
6466  */
6467 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
6468         // Configuration initialization
6469         config = config || {};
6471         // Properties (must be done before parent constructor which calls #setDisabled)
6472         this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
6474         // Parent constructor
6475         OO.ui.RadioOptionWidget.parent.call( this, config );
6477         // Initialization
6478         // Remove implicit role, we're handling it ourselves
6479         this.radio.$input.attr( 'role', 'presentation' );
6480         this.$element
6481                 .addClass( 'oo-ui-radioOptionWidget' )
6482                 .attr( 'role', 'radio' )
6483                 .attr( 'aria-checked', 'false' )
6484                 .removeAttr( 'aria-selected' )
6485                 .prepend( this.radio.$element );
6488 /* Setup */
6490 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
6492 /* Static Properties */
6494 OO.ui.RadioOptionWidget.static.highlightable = false;
6496 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
6498 OO.ui.RadioOptionWidget.static.pressable = false;
6500 OO.ui.RadioOptionWidget.static.tagName = 'label';
6502 /* Methods */
6505  * @inheritdoc
6506  */
6507 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
6508         OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
6510         this.radio.setSelected( state );
6511         this.$element
6512                 .attr( 'aria-checked', state.toString() )
6513                 .removeAttr( 'aria-selected' );
6515         return this;
6519  * @inheritdoc
6520  */
6521 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
6522         OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
6524         this.radio.setDisabled( this.isDisabled() );
6526         return this;
6530  * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
6531  * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
6532  * an interface for adding, removing and selecting options.
6533  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6535  * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6536  * OO.ui.RadioSelectInputWidget instead.
6538  *     @example
6539  *     // A RadioSelectWidget with RadioOptions.
6540  *     var option1 = new OO.ui.RadioOptionWidget( {
6541  *         data: 'a',
6542  *         label: 'Selected radio option'
6543  *     } );
6545  *     var option2 = new OO.ui.RadioOptionWidget( {
6546  *         data: 'b',
6547  *         label: 'Unselected radio option'
6548  *     } );
6550  *     var radioSelect=new OO.ui.RadioSelectWidget( {
6551  *         items: [ option1, option2 ]
6552  *      } );
6554  *     // Select 'option 1' using the RadioSelectWidget's selectItem() method.
6555  *     radioSelect.selectItem( option1 );
6557  *     $( 'body' ).append( radioSelect.$element );
6559  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6562  * @class
6563  * @extends OO.ui.SelectWidget
6564  * @mixins OO.ui.mixin.TabIndexedElement
6566  * @constructor
6567  * @param {Object} [config] Configuration options
6568  */
6569 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
6570         // Parent constructor
6571         OO.ui.RadioSelectWidget.parent.call( this, config );
6573         // Mixin constructors
6574         OO.ui.mixin.TabIndexedElement.call( this, config );
6576         // Events
6577         this.$element.on( {
6578                 focus: this.bindKeyDownListener.bind( this ),
6579                 blur: this.unbindKeyDownListener.bind( this )
6580         } );
6582         // Initialization
6583         this.$element
6584                 .addClass( 'oo-ui-radioSelectWidget' )
6585                 .attr( 'role', 'radiogroup' );
6588 /* Setup */
6590 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
6591 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
6594  * MultioptionWidgets are special elements that can be selected and configured with data. The
6595  * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
6596  * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6597  * and examples, please see the [OOjs UI documentation on MediaWiki][1].
6599  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
6601  * @class
6602  * @extends OO.ui.Widget
6603  * @mixins OO.ui.mixin.ItemWidget
6604  * @mixins OO.ui.mixin.LabelElement
6606  * @constructor
6607  * @param {Object} [config] Configuration options
6608  * @cfg {boolean} [selected=false] Whether the option is initially selected
6609  */
6610 OO.ui.MultioptionWidget = function OoUiMultioptionWidget( config ) {
6611         // Configuration initialization
6612         config = config || {};
6614         // Parent constructor
6615         OO.ui.MultioptionWidget.parent.call( this, config );
6617         // Mixin constructors
6618         OO.ui.mixin.ItemWidget.call( this );
6619         OO.ui.mixin.LabelElement.call( this, config );
6621         // Properties
6622         this.selected = null;
6624         // Initialization
6625         this.$element
6626                 .addClass( 'oo-ui-multioptionWidget' )
6627                 .append( this.$label );
6628         this.setSelected( config.selected );
6631 /* Setup */
6633 OO.inheritClass( OO.ui.MultioptionWidget, OO.ui.Widget );
6634 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.ItemWidget );
6635 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.LabelElement );
6637 /* Events */
6640  * @event change
6642  * A change event is emitted when the selected state of the option changes.
6644  * @param {boolean} selected Whether the option is now selected
6645  */
6647 /* Methods */
6650  * Check if the option is selected.
6652  * @return {boolean} Item is selected
6653  */
6654 OO.ui.MultioptionWidget.prototype.isSelected = function () {
6655         return this.selected;
6659  * Set the option’s selected state. In general, all modifications to the selection
6660  * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6661  * method instead of this method.
6663  * @param {boolean} [state=false] Select option
6664  * @chainable
6665  */
6666 OO.ui.MultioptionWidget.prototype.setSelected = function ( state ) {
6667         state = !!state;
6668         if ( this.selected !== state ) {
6669                 this.selected = state;
6670                 this.emit( 'change', state );
6671                 this.$element.toggleClass( 'oo-ui-multioptionWidget-selected', state );
6672         }
6673         return this;
6677  * MultiselectWidget allows selecting multiple options from a list.
6679  * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
6681  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6683  * @class
6684  * @abstract
6685  * @extends OO.ui.Widget
6686  * @mixins OO.ui.mixin.GroupWidget
6688  * @constructor
6689  * @param {Object} [config] Configuration options
6690  * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
6691  */
6692 OO.ui.MultiselectWidget = function OoUiMultiselectWidget( config ) {
6693         // Parent constructor
6694         OO.ui.MultiselectWidget.parent.call( this, config );
6696         // Configuration initialization
6697         config = config || {};
6699         // Mixin constructors
6700         OO.ui.mixin.GroupWidget.call( this, config );
6702         // Events
6703         this.aggregate( { change: 'select' } );
6704         // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
6705         // by GroupElement only when items are added/removed
6706         this.connect( this, { select: [ 'emit', 'change' ] } );
6708         // Initialization
6709         if ( config.items ) {
6710                 this.addItems( config.items );
6711         }
6712         this.$group.addClass( 'oo-ui-multiselectWidget-group' );
6713         this.$element.addClass( 'oo-ui-multiselectWidget' )
6714                 .append( this.$group );
6717 /* Setup */
6719 OO.inheritClass( OO.ui.MultiselectWidget, OO.ui.Widget );
6720 OO.mixinClass( OO.ui.MultiselectWidget, OO.ui.mixin.GroupWidget );
6722 /* Events */
6725  * @event change
6727  * A change event is emitted when the set of items changes, or an item is selected or deselected.
6728  */
6731  * @event select
6733  * A select event is emitted when an item is selected or deselected.
6734  */
6736 /* Methods */
6739  * Get options that are selected.
6741  * @return {OO.ui.MultioptionWidget[]} Selected options
6742  */
6743 OO.ui.MultiselectWidget.prototype.getSelectedItems = function () {
6744         return this.items.filter( function ( item ) {
6745                 return item.isSelected();
6746         } );
6750  * Get the data of options that are selected.
6752  * @return {Object[]|string[]} Values of selected options
6753  */
6754 OO.ui.MultiselectWidget.prototype.getSelectedItemsData = function () {
6755         return this.getSelectedItems().map( function ( item ) {
6756                 return item.data;
6757         } );
6761  * Select options by reference. Options not mentioned in the `items` array will be deselected.
6763  * @param {OO.ui.MultioptionWidget[]} items Items to select
6764  * @chainable
6765  */
6766 OO.ui.MultiselectWidget.prototype.selectItems = function ( items ) {
6767         this.items.forEach( function ( item ) {
6768                 var selected = items.indexOf( item ) !== -1;
6769                 item.setSelected( selected );
6770         } );
6771         return this;
6775  * Select items by their data. Options not mentioned in the `datas` array will be deselected.
6777  * @param {Object[]|string[]} datas Values of items to select
6778  * @chainable
6779  */
6780 OO.ui.MultiselectWidget.prototype.selectItemsByData = function ( datas ) {
6781         var items,
6782                 widget = this;
6783         items = datas.map( function ( data ) {
6784                 return widget.getItemFromData( data );
6785         } );
6786         this.selectItems( items );
6787         return this;
6791  * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
6792  * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
6793  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6795  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6797  * @class
6798  * @extends OO.ui.MultioptionWidget
6800  * @constructor
6801  * @param {Object} [config] Configuration options
6802  */
6803 OO.ui.CheckboxMultioptionWidget = function OoUiCheckboxMultioptionWidget( config ) {
6804         // Configuration initialization
6805         config = config || {};
6807         // Properties (must be done before parent constructor which calls #setDisabled)
6808         this.checkbox = new OO.ui.CheckboxInputWidget();
6810         // Parent constructor
6811         OO.ui.CheckboxMultioptionWidget.parent.call( this, config );
6813         // Events
6814         this.checkbox.on( 'change', this.onCheckboxChange.bind( this ) );
6815         this.$element.on( 'keydown', this.onKeyDown.bind( this ) );
6817         // Initialization
6818         this.$element
6819                 .addClass( 'oo-ui-checkboxMultioptionWidget' )
6820                 .prepend( this.checkbox.$element );
6823 /* Setup */
6825 OO.inheritClass( OO.ui.CheckboxMultioptionWidget, OO.ui.MultioptionWidget );
6827 /* Static Properties */
6829 OO.ui.CheckboxMultioptionWidget.static.tagName = 'label';
6831 /* Methods */
6834  * Handle checkbox selected state change.
6836  * @private
6837  */
6838 OO.ui.CheckboxMultioptionWidget.prototype.onCheckboxChange = function () {
6839         this.setSelected( this.checkbox.isSelected() );
6843  * @inheritdoc
6844  */
6845 OO.ui.CheckboxMultioptionWidget.prototype.setSelected = function ( state ) {
6846         OO.ui.CheckboxMultioptionWidget.parent.prototype.setSelected.call( this, state );
6847         this.checkbox.setSelected( state );
6848         return this;
6852  * @inheritdoc
6853  */
6854 OO.ui.CheckboxMultioptionWidget.prototype.setDisabled = function ( disabled ) {
6855         OO.ui.CheckboxMultioptionWidget.parent.prototype.setDisabled.call( this, disabled );
6856         this.checkbox.setDisabled( this.isDisabled() );
6857         return this;
6861  * Focus the widget.
6862  */
6863 OO.ui.CheckboxMultioptionWidget.prototype.focus = function () {
6864         this.checkbox.focus();
6868  * Handle key down events.
6870  * @protected
6871  * @param {jQuery.Event} e
6872  */
6873 OO.ui.CheckboxMultioptionWidget.prototype.onKeyDown = function ( e ) {
6874         var
6875                 element = this.getElementGroup(),
6876                 nextItem;
6878         if ( e.keyCode === OO.ui.Keys.LEFT || e.keyCode === OO.ui.Keys.UP ) {
6879                 nextItem = element.getRelativeFocusableItem( this, -1 );
6880         } else if ( e.keyCode === OO.ui.Keys.RIGHT || e.keyCode === OO.ui.Keys.DOWN ) {
6881                 nextItem = element.getRelativeFocusableItem( this, 1 );
6882         }
6884         if ( nextItem ) {
6885                 e.preventDefault();
6886                 nextItem.focus();
6887         }
6891  * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
6892  * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
6893  * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
6894  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6896  * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6897  * OO.ui.CheckboxMultiselectInputWidget instead.
6899  *     @example
6900  *     // A CheckboxMultiselectWidget with CheckboxMultioptions.
6901  *     var option1 = new OO.ui.CheckboxMultioptionWidget( {
6902  *         data: 'a',
6903  *         selected: true,
6904  *         label: 'Selected checkbox'
6905  *     } );
6907  *     var option2 = new OO.ui.CheckboxMultioptionWidget( {
6908  *         data: 'b',
6909  *         label: 'Unselected checkbox'
6910  *     } );
6912  *     var multiselect=new OO.ui.CheckboxMultiselectWidget( {
6913  *         items: [ option1, option2 ]
6914  *      } );
6916  *     $( 'body' ).append( multiselect.$element );
6918  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6920  * @class
6921  * @extends OO.ui.MultiselectWidget
6923  * @constructor
6924  * @param {Object} [config] Configuration options
6925  */
6926 OO.ui.CheckboxMultiselectWidget = function OoUiCheckboxMultiselectWidget( config ) {
6927         // Parent constructor
6928         OO.ui.CheckboxMultiselectWidget.parent.call( this, config );
6930         // Properties
6931         this.$lastClicked = null;
6933         // Events
6934         this.$group.on( 'click', this.onClick.bind( this ) );
6936         // Initialization
6937         this.$element
6938                 .addClass( 'oo-ui-checkboxMultiselectWidget' );
6941 /* Setup */
6943 OO.inheritClass( OO.ui.CheckboxMultiselectWidget, OO.ui.MultiselectWidget );
6945 /* Methods */
6948  * Get an option by its position relative to the specified item (or to the start of the option array,
6949  * if item is `null`). The direction in which to search through the option array is specified with a
6950  * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6951  * `null` if there are no options in the array.
6953  * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6954  * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6955  * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
6956  */
6957 OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function ( item, direction ) {
6958         var currentIndex, nextIndex, i,
6959                 increase = direction > 0 ? 1 : -1,
6960                 len = this.items.length;
6962         if ( item ) {
6963                 currentIndex = this.items.indexOf( item );
6964                 nextIndex = ( currentIndex + increase + len ) % len;
6965         } else {
6966                 // If no item is selected and moving forward, start at the beginning.
6967                 // If moving backward, start at the end.
6968                 nextIndex = direction > 0 ? 0 : len - 1;
6969         }
6971         for ( i = 0; i < len; i++ ) {
6972                 item = this.items[ nextIndex ];
6973                 if ( item && !item.isDisabled() ) {
6974                         return item;
6975                 }
6976                 nextIndex = ( nextIndex + increase + len ) % len;
6977         }
6978         return null;
6982  * Handle click events on checkboxes.
6984  * @param {jQuery.Event} e
6985  */
6986 OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
6987         var $options, checked,
6988                 $lastClicked = this.$lastClicked,
6989                 $nowClicked = $( e.target ).closest( '.oo-ui-checkboxMultioptionWidget' )
6990                         .not( '.oo-ui-widget-disabled' );
6992         // Allow selecting multiple options at once by Shift-clicking them
6993         if ( $lastClicked && $nowClicked.length && e.shiftKey ) {
6994                 $options = this.$group.find( '.oo-ui-checkboxMultioptionWidget' );
6995                 checked = $nowClicked.find( 'input' ).prop( 'checked' );
6997                 $options
6998                         .slice(
6999                                 Math.min( $options.index( $lastClicked ), $options.index( $nowClicked ) ),
7000                                 Math.max( $options.index( $lastClicked ), $options.index( $nowClicked ) ) + 1
7001                         )
7002                         .find( 'input' )
7003                         .filter( function () {
7004                                 return !this.disabled;
7005                         } )
7006                         .prop( 'checked', checked )
7007                         .trigger( 'change' );
7008         }
7010         if ( $nowClicked.length ) {
7011                 this.$lastClicked = $nowClicked;
7012         }
7016  * Element that will stick under a specified container, even when it is inserted elsewhere in the
7017  * document (for example, in a OO.ui.Window's $overlay).
7019  * The elements's position is automatically calculated and maintained when window is resized or the
7020  * page is scrolled. If you reposition the container manually, you have to call #position to make
7021  * sure the element is still placed correctly.
7023  * As positioning is only possible when both the element and the container are attached to the DOM
7024  * and visible, it's only done after you call #togglePositioning. You might want to do this inside
7025  * the #toggle method to display a floating popup, for example.
7027  * @abstract
7028  * @class
7030  * @constructor
7031  * @param {Object} [config] Configuration options
7032  * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
7033  * @cfg {jQuery} [$floatableContainer] Node to position below
7034  */
7035 OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
7036         // Configuration initialization
7037         config = config || {};
7039         // Properties
7040         this.$floatable = null;
7041         this.$floatableContainer = null;
7042         this.$floatableWindow = null;
7043         this.$floatableClosestScrollable = null;
7044         this.onFloatableScrollHandler = this.position.bind( this );
7045         this.onFloatableWindowResizeHandler = this.position.bind( this );
7047         // Initialization
7048         this.setFloatableContainer( config.$floatableContainer );
7049         this.setFloatableElement( config.$floatable || this.$element );
7052 /* Methods */
7055  * Set floatable element.
7057  * If an element is already set, it will be cleaned up before setting up the new element.
7059  * @param {jQuery} $floatable Element to make floatable
7060  */
7061 OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
7062         if ( this.$floatable ) {
7063                 this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
7064                 this.$floatable.css( { left: '', top: '' } );
7065         }
7067         this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
7068         this.position();
7072  * Set floatable container.
7074  * The element will be always positioned under the specified container.
7076  * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
7077  */
7078 OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
7079         this.$floatableContainer = $floatableContainer;
7080         if ( this.$floatable ) {
7081                 this.position();
7082         }
7086  * Toggle positioning.
7088  * Do not turn positioning on until after the element is attached to the DOM and visible.
7090  * @param {boolean} [positioning] Enable positioning, omit to toggle
7091  * @chainable
7092  */
7093 OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
7094         var closestScrollableOfContainer, closestScrollableOfFloatable;
7096         positioning = positioning === undefined ? !this.positioning : !!positioning;
7098         if ( this.positioning !== positioning ) {
7099                 this.positioning = positioning;
7101                 closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
7102                 closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
7103                 this.needsCustomPosition = closestScrollableOfContainer !== closestScrollableOfFloatable;
7104                 // If the scrollable is the root, we have to listen to scroll events
7105                 // on the window because of browser inconsistencies.
7106                 if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
7107                         closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
7108                 }
7110                 if ( positioning ) {
7111                         this.$floatableWindow = $( this.getElementWindow() );
7112                         this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
7114                         this.$floatableClosestScrollable = $( closestScrollableOfContainer );
7115                         this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
7117                         // Initial position after visible
7118                         this.position();
7119                 } else {
7120                         if ( this.$floatableWindow ) {
7121                                 this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
7122                                 this.$floatableWindow = null;
7123                         }
7125                         if ( this.$floatableClosestScrollable ) {
7126                                 this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
7127                                 this.$floatableClosestScrollable = null;
7128                         }
7130                         this.$floatable.css( { left: '', top: '' } );
7131                 }
7132         }
7134         return this;
7138  * Check whether the bottom edge of the given element is within the viewport of the given container.
7140  * @private
7141  * @param {jQuery} $element
7142  * @param {jQuery} $container
7143  * @return {boolean}
7144  */
7145 OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
7146         var elemRect, contRect,
7147                 topEdgeInBounds = false,
7148                 leftEdgeInBounds = false,
7149                 bottomEdgeInBounds = false,
7150                 rightEdgeInBounds = false;
7152         elemRect = $element[ 0 ].getBoundingClientRect();
7153         if ( $container[ 0 ] === window ) {
7154                 contRect = {
7155                         top: 0,
7156                         left: 0,
7157                         right: document.documentElement.clientWidth,
7158                         bottom: document.documentElement.clientHeight
7159                 };
7160         } else {
7161                 contRect = $container[ 0 ].getBoundingClientRect();
7162         }
7164         if ( elemRect.top >= contRect.top && elemRect.top <= contRect.bottom ) {
7165                 topEdgeInBounds = true;
7166         }
7167         if ( elemRect.left >= contRect.left && elemRect.left <= contRect.right ) {
7168                 leftEdgeInBounds = true;
7169         }
7170         if ( elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom ) {
7171                 bottomEdgeInBounds = true;
7172         }
7173         if ( elemRect.right >= contRect.left && elemRect.right <= contRect.right ) {
7174                 rightEdgeInBounds = true;
7175         }
7177         // We only care that any part of the bottom edge is visible
7178         return bottomEdgeInBounds && ( leftEdgeInBounds || rightEdgeInBounds );
7182  * Position the floatable below its container.
7184  * This should only be done when both of them are attached to the DOM and visible.
7186  * @chainable
7187  */
7188 OO.ui.mixin.FloatableElement.prototype.position = function () {
7189         var pos;
7191         if ( !this.positioning ) {
7192                 return this;
7193         }
7195         if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
7196                 this.$floatable.addClass( 'oo-ui-floatableElement-hidden' );
7197                 return;
7198         } else {
7199                 this.$floatable.removeClass( 'oo-ui-floatableElement-hidden' );
7200         }
7202         if ( !this.needsCustomPosition ) {
7203                 return;
7204         }
7206         pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
7208         // Position under container
7209         pos.top += this.$floatableContainer.height();
7210         this.$floatable.css( pos );
7212         // We updated the position, so re-evaluate the clipping state.
7213         // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
7214         // will not notice the need to update itself.)
7215         // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
7216         // it not listen to the right events in the right places?
7217         if ( this.clip ) {
7218                 this.clip();
7219         }
7221         return this;
7225  * FloatingMenuSelectWidget is a menu that will stick under a specified
7226  * container, even when it is inserted elsewhere in the document (for example,
7227  * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
7228  * menu from being clipped too aggresively.
7230  * The menu's position is automatically calculated and maintained when the menu
7231  * is toggled or the window is resized.
7233  * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
7235  * @class
7236  * @extends OO.ui.MenuSelectWidget
7237  * @mixins OO.ui.mixin.FloatableElement
7239  * @constructor
7240  * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
7241  *   Deprecated, omit this parameter and specify `$container` instead.
7242  * @param {Object} [config] Configuration options
7243  * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
7244  */
7245 OO.ui.FloatingMenuSelectWidget = function OoUiFloatingMenuSelectWidget( inputWidget, config ) {
7246         // Allow 'inputWidget' parameter and config for backwards compatibility
7247         if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
7248                 config = inputWidget;
7249                 inputWidget = config.inputWidget;
7250         }
7252         // Configuration initialization
7253         config = config || {};
7255         // Parent constructor
7256         OO.ui.FloatingMenuSelectWidget.parent.call( this, config );
7258         // Properties (must be set before mixin constructors)
7259         this.inputWidget = inputWidget; // For backwards compatibility
7260         this.$container = config.$container || this.inputWidget.$element;
7262         // Mixins constructors
7263         OO.ui.mixin.FloatableElement.call( this, $.extend( {}, config, { $floatableContainer: this.$container } ) );
7265         // Initialization
7266         this.$element.addClass( 'oo-ui-floatingMenuSelectWidget' );
7267         // For backwards compatibility
7268         this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
7271 /* Setup */
7273 OO.inheritClass( OO.ui.FloatingMenuSelectWidget, OO.ui.MenuSelectWidget );
7274 OO.mixinClass( OO.ui.FloatingMenuSelectWidget, OO.ui.mixin.FloatableElement );
7276 // For backwards compatibility
7277 OO.ui.TextInputMenuSelectWidget = OO.ui.FloatingMenuSelectWidget;
7279 /* Methods */
7282  * @inheritdoc
7283  */
7284 OO.ui.FloatingMenuSelectWidget.prototype.toggle = function ( visible ) {
7285         var change;
7286         visible = visible === undefined ? !this.isVisible() : !!visible;
7287         change = visible !== this.isVisible();
7289         if ( change && visible ) {
7290                 // Make sure the width is set before the parent method runs.
7291                 this.setIdealSize( this.$container.width() );
7292         }
7294         // Parent method
7295         // This will call this.clip(), which is nonsensical since we're not positioned yet...
7296         OO.ui.FloatingMenuSelectWidget.parent.prototype.toggle.call( this, visible );
7298         if ( change ) {
7299                 this.togglePositioning( this.isVisible() );
7300         }
7302         return this;
7306  * InputWidget is the base class for all input widgets, which
7307  * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
7308  * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
7309  * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7311  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7313  * @abstract
7314  * @class
7315  * @extends OO.ui.Widget
7316  * @mixins OO.ui.mixin.FlaggedElement
7317  * @mixins OO.ui.mixin.TabIndexedElement
7318  * @mixins OO.ui.mixin.TitledElement
7319  * @mixins OO.ui.mixin.AccessKeyedElement
7321  * @constructor
7322  * @param {Object} [config] Configuration options
7323  * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
7324  * @cfg {string} [value=''] The value of the input.
7325  * @cfg {string} [dir] The directionality of the input (ltr/rtl).
7326  * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
7327  *  before it is accepted.
7328  */
7329 OO.ui.InputWidget = function OoUiInputWidget( config ) {
7330         // Configuration initialization
7331         config = config || {};
7333         // Parent constructor
7334         OO.ui.InputWidget.parent.call( this, config );
7336         // Properties
7337         // See #reusePreInfuseDOM about config.$input
7338         this.$input = config.$input || this.getInputElement( config );
7339         this.value = '';
7340         this.inputFilter = config.inputFilter;
7342         // Mixin constructors
7343         OO.ui.mixin.FlaggedElement.call( this, config );
7344         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
7345         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
7346         OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$input } ) );
7348         // Events
7349         this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
7351         // Initialization
7352         this.$input
7353                 .addClass( 'oo-ui-inputWidget-input' )
7354                 .attr( 'name', config.name )
7355                 .prop( 'disabled', this.isDisabled() );
7356         this.$element
7357                 .addClass( 'oo-ui-inputWidget' )
7358                 .append( this.$input );
7359         this.setValue( config.value );
7360         if ( config.dir ) {
7361                 this.setDir( config.dir );
7362         }
7365 /* Setup */
7367 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
7368 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
7369 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
7370 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
7371 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
7373 /* Static Properties */
7375 OO.ui.InputWidget.static.supportsSimpleLabel = true;
7377 /* Static Methods */
7380  * @inheritdoc
7381  */
7382 OO.ui.InputWidget.static.reusePreInfuseDOM = function ( node, config ) {
7383         config = OO.ui.InputWidget.parent.static.reusePreInfuseDOM( node, config );
7384         // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
7385         config.$input = $( node ).find( '.oo-ui-inputWidget-input' );
7386         return config;
7390  * @inheritdoc
7391  */
7392 OO.ui.InputWidget.static.gatherPreInfuseState = function ( node, config ) {
7393         var state = OO.ui.InputWidget.parent.static.gatherPreInfuseState( node, config );
7394         if ( config.$input && config.$input.length ) {
7395                 state.value = config.$input.val();
7396                 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
7397                 state.focus = config.$input.is( ':focus' );
7398         }
7399         return state;
7402 /* Events */
7405  * @event change
7407  * A change event is emitted when the value of the input changes.
7409  * @param {string} value
7410  */
7412 /* Methods */
7415  * Get input element.
7417  * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
7418  * different circumstances. The element must have a `value` property (like form elements).
7420  * @protected
7421  * @param {Object} config Configuration options
7422  * @return {jQuery} Input element
7423  */
7424 OO.ui.InputWidget.prototype.getInputElement = function () {
7425         return $( '<input>' );
7429  * Handle potentially value-changing events.
7431  * @private
7432  * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
7433  */
7434 OO.ui.InputWidget.prototype.onEdit = function () {
7435         var widget = this;
7436         if ( !this.isDisabled() ) {
7437                 // Allow the stack to clear so the value will be updated
7438                 setTimeout( function () {
7439                         widget.setValue( widget.$input.val() );
7440                 } );
7441         }
7445  * Get the value of the input.
7447  * @return {string} Input value
7448  */
7449 OO.ui.InputWidget.prototype.getValue = function () {
7450         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7451         // it, and we won't know unless they're kind enough to trigger a 'change' event.
7452         var value = this.$input.val();
7453         if ( this.value !== value ) {
7454                 this.setValue( value );
7455         }
7456         return this.value;
7460  * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
7462  * @deprecated since v0.13.1; use #setDir directly
7463  * @param {boolean} isRTL Directionality is right-to-left
7464  * @chainable
7465  */
7466 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
7467         this.setDir( isRTL ? 'rtl' : 'ltr' );
7468         return this;
7472  * Set the directionality of the input.
7474  * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
7475  * @chainable
7476  */
7477 OO.ui.InputWidget.prototype.setDir = function ( dir ) {
7478         this.$input.prop( 'dir', dir );
7479         return this;
7483  * Set the value of the input.
7485  * @param {string} value New value
7486  * @fires change
7487  * @chainable
7488  */
7489 OO.ui.InputWidget.prototype.setValue = function ( value ) {
7490         value = this.cleanUpValue( value );
7491         // Update the DOM if it has changed. Note that with cleanUpValue, it
7492         // is possible for the DOM value to change without this.value changing.
7493         if ( this.$input.val() !== value ) {
7494                 this.$input.val( value );
7495         }
7496         if ( this.value !== value ) {
7497                 this.value = value;
7498                 this.emit( 'change', this.value );
7499         }
7500         return this;
7504  * Clean up incoming value.
7506  * Ensures value is a string, and converts undefined and null to empty string.
7508  * @private
7509  * @param {string} value Original value
7510  * @return {string} Cleaned up value
7511  */
7512 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
7513         if ( value === undefined || value === null ) {
7514                 return '';
7515         } else if ( this.inputFilter ) {
7516                 return this.inputFilter( String( value ) );
7517         } else {
7518                 return String( value );
7519         }
7523  * Simulate the behavior of clicking on a label bound to this input. This method is only called by
7524  * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
7525  * called directly.
7526  */
7527 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
7528         if ( !this.isDisabled() ) {
7529                 if ( this.$input.is( ':checkbox, :radio' ) ) {
7530                         this.$input.click();
7531                 }
7532                 if ( this.$input.is( ':input' ) ) {
7533                         this.$input[ 0 ].focus();
7534                 }
7535         }
7539  * @inheritdoc
7540  */
7541 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
7542         OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
7543         if ( this.$input ) {
7544                 this.$input.prop( 'disabled', this.isDisabled() );
7545         }
7546         return this;
7550  * Focus the input.
7552  * @chainable
7553  */
7554 OO.ui.InputWidget.prototype.focus = function () {
7555         this.$input[ 0 ].focus();
7556         return this;
7560  * Blur the input.
7562  * @chainable
7563  */
7564 OO.ui.InputWidget.prototype.blur = function () {
7565         this.$input[ 0 ].blur();
7566         return this;
7570  * @inheritdoc
7571  */
7572 OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
7573         OO.ui.InputWidget.parent.prototype.restorePreInfuseState.call( this, state );
7574         if ( state.value !== undefined && state.value !== this.getValue() ) {
7575                 this.setValue( state.value );
7576         }
7577         if ( state.focus ) {
7578                 this.focus();
7579         }
7583  * ButtonInputWidget is used to submit HTML forms and is intended to be used within
7584  * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
7585  * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
7586  * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
7587  * [OOjs UI documentation on MediaWiki] [1] for more information.
7589  *     @example
7590  *     // A ButtonInputWidget rendered as an HTML button, the default.
7591  *     var button = new OO.ui.ButtonInputWidget( {
7592  *         label: 'Input button',
7593  *         icon: 'check',
7594  *         value: 'check'
7595  *     } );
7596  *     $( 'body' ).append( button.$element );
7598  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
7600  * @class
7601  * @extends OO.ui.InputWidget
7602  * @mixins OO.ui.mixin.ButtonElement
7603  * @mixins OO.ui.mixin.IconElement
7604  * @mixins OO.ui.mixin.IndicatorElement
7605  * @mixins OO.ui.mixin.LabelElement
7606  * @mixins OO.ui.mixin.TitledElement
7608  * @constructor
7609  * @param {Object} [config] Configuration options
7610  * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
7611  * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
7612  *  Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
7613  *  non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
7614  *  be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
7615  */
7616 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
7617         // Configuration initialization
7618         config = $.extend( { type: 'button', useInputTag: false }, config );
7620         // See InputWidget#reusePreInfuseDOM about config.$input
7621         if ( config.$input ) {
7622                 config.$input.empty();
7623         }
7625         // Properties (must be set before parent constructor, which calls #setValue)
7626         this.useInputTag = config.useInputTag;
7628         // Parent constructor
7629         OO.ui.ButtonInputWidget.parent.call( this, config );
7631         // Mixin constructors
7632         OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
7633         OO.ui.mixin.IconElement.call( this, config );
7634         OO.ui.mixin.IndicatorElement.call( this, config );
7635         OO.ui.mixin.LabelElement.call( this, config );
7636         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
7638         // Initialization
7639         if ( !config.useInputTag ) {
7640                 this.$input.append( this.$icon, this.$label, this.$indicator );
7641         }
7642         this.$element.addClass( 'oo-ui-buttonInputWidget' );
7645 /* Setup */
7647 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
7648 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
7649 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
7650 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
7651 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
7652 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
7654 /* Static Properties */
7657  * Disable generating `<label>` elements for buttons. One would very rarely need additional label
7658  * for a button, and it's already a big clickable target, and it causes unexpected rendering.
7659  */
7660 OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
7662 /* Methods */
7665  * @inheritdoc
7666  * @protected
7667  */
7668 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
7669         var type;
7670         type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ? config.type : 'button';
7671         return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
7675  * Set label value.
7677  * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
7679  * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
7680  *  text, or `null` for no label
7681  * @chainable
7682  */
7683 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
7684         if ( typeof label === 'function' ) {
7685                 label = OO.ui.resolveMsg( label );
7686         }
7688         if ( this.useInputTag ) {
7689                 // Discard non-plaintext labels
7690                 if ( typeof label !== 'string' ) {
7691                         label = '';
7692                 }
7694                 this.$input.val( label );
7695         }
7697         return OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
7701  * Set the value of the input.
7703  * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
7704  * they do not support {@link #value values}.
7706  * @param {string} value New value
7707  * @chainable
7708  */
7709 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
7710         if ( !this.useInputTag ) {
7711                 OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
7712         }
7713         return this;
7717  * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
7718  * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
7719  * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
7720  * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
7722  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7724  *     @example
7725  *     // An example of selected, unselected, and disabled checkbox inputs
7726  *     var checkbox1=new OO.ui.CheckboxInputWidget( {
7727  *          value: 'a',
7728  *          selected: true
7729  *     } );
7730  *     var checkbox2=new OO.ui.CheckboxInputWidget( {
7731  *         value: 'b'
7732  *     } );
7733  *     var checkbox3=new OO.ui.CheckboxInputWidget( {
7734  *         value:'c',
7735  *         disabled: true
7736  *     } );
7737  *     // Create a fieldset layout with fields for each checkbox.
7738  *     var fieldset = new OO.ui.FieldsetLayout( {
7739  *         label: 'Checkboxes'
7740  *     } );
7741  *     fieldset.addItems( [
7742  *         new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
7743  *         new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
7744  *         new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
7745  *     ] );
7746  *     $( 'body' ).append( fieldset.$element );
7748  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7750  * @class
7751  * @extends OO.ui.InputWidget
7753  * @constructor
7754  * @param {Object} [config] Configuration options
7755  * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
7756  */
7757 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
7758         // Configuration initialization
7759         config = config || {};
7761         // Parent constructor
7762         OO.ui.CheckboxInputWidget.parent.call( this, config );
7764         // Initialization
7765         this.$element
7766                 .addClass( 'oo-ui-checkboxInputWidget' )
7767                 // Required for pretty styling in MediaWiki theme
7768                 .append( $( '<span>' ) );
7769         this.setSelected( config.selected !== undefined ? config.selected : false );
7772 /* Setup */
7774 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
7776 /* Static Methods */
7779  * @inheritdoc
7780  */
7781 OO.ui.CheckboxInputWidget.static.gatherPreInfuseState = function ( node, config ) {
7782         var state = OO.ui.CheckboxInputWidget.parent.static.gatherPreInfuseState( node, config );
7783         state.checked = config.$input.prop( 'checked' );
7784         return state;
7787 /* Methods */
7790  * @inheritdoc
7791  * @protected
7792  */
7793 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
7794         return $( '<input>' ).attr( 'type', 'checkbox' );
7798  * @inheritdoc
7799  */
7800 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
7801         var widget = this;
7802         if ( !this.isDisabled() ) {
7803                 // Allow the stack to clear so the value will be updated
7804                 setTimeout( function () {
7805                         widget.setSelected( widget.$input.prop( 'checked' ) );
7806                 } );
7807         }
7811  * Set selection state of this checkbox.
7813  * @param {boolean} state `true` for selected
7814  * @chainable
7815  */
7816 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
7817         state = !!state;
7818         if ( this.selected !== state ) {
7819                 this.selected = state;
7820                 this.$input.prop( 'checked', this.selected );
7821                 this.emit( 'change', this.selected );
7822         }
7823         return this;
7827  * Check if this checkbox is selected.
7829  * @return {boolean} Checkbox is selected
7830  */
7831 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
7832         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7833         // it, and we won't know unless they're kind enough to trigger a 'change' event.
7834         var selected = this.$input.prop( 'checked' );
7835         if ( this.selected !== selected ) {
7836                 this.setSelected( selected );
7837         }
7838         return this.selected;
7842  * @inheritdoc
7843  */
7844 OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
7845         OO.ui.CheckboxInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
7846         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
7847                 this.setSelected( state.checked );
7848         }
7852  * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
7853  * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7854  * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7855  * more information about input widgets.
7857  * A DropdownInputWidget always has a value (one of the options is always selected), unless there
7858  * are no options. If no `value` configuration option is provided, the first option is selected.
7859  * If you need a state representing no value (no option being selected), use a DropdownWidget.
7861  * This and OO.ui.RadioSelectInputWidget support the same configuration options.
7863  *     @example
7864  *     // Example: A DropdownInputWidget with three options
7865  *     var dropdownInput = new OO.ui.DropdownInputWidget( {
7866  *         options: [
7867  *             { data: 'a', label: 'First' },
7868  *             { data: 'b', label: 'Second'},
7869  *             { data: 'c', label: 'Third' }
7870  *         ]
7871  *     } );
7872  *     $( 'body' ).append( dropdownInput.$element );
7874  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7876  * @class
7877  * @extends OO.ui.InputWidget
7878  * @mixins OO.ui.mixin.TitledElement
7880  * @constructor
7881  * @param {Object} [config] Configuration options
7882  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7883  * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
7884  */
7885 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
7886         // Configuration initialization
7887         config = config || {};
7889         // See InputWidget#reusePreInfuseDOM about config.$input
7890         if ( config.$input ) {
7891                 config.$input.addClass( 'oo-ui-element-hidden' );
7892         }
7894         // Properties (must be done before parent constructor which calls #setDisabled)
7895         this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
7897         // Parent constructor
7898         OO.ui.DropdownInputWidget.parent.call( this, config );
7900         // Mixin constructors
7901         OO.ui.mixin.TitledElement.call( this, config );
7903         // Events
7904         this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
7906         // Initialization
7907         this.setOptions( config.options || [] );
7908         this.$element
7909                 .addClass( 'oo-ui-dropdownInputWidget' )
7910                 .append( this.dropdownWidget.$element );
7913 /* Setup */
7915 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
7916 OO.mixinClass( OO.ui.DropdownInputWidget, OO.ui.mixin.TitledElement );
7918 /* Methods */
7921  * @inheritdoc
7922  * @protected
7923  */
7924 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
7925         return $( '<input>' ).attr( 'type', 'hidden' );
7929  * Handles menu select events.
7931  * @private
7932  * @param {OO.ui.MenuOptionWidget} item Selected menu item
7933  */
7934 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
7935         this.setValue( item.getData() );
7939  * @inheritdoc
7940  */
7941 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
7942         value = this.cleanUpValue( value );
7943         this.dropdownWidget.getMenu().selectItemByData( value );
7944         OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
7945         return this;
7949  * @inheritdoc
7950  */
7951 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
7952         this.dropdownWidget.setDisabled( state );
7953         OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
7954         return this;
7958  * Set the options available for this input.
7960  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
7961  * @chainable
7962  */
7963 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
7964         var
7965                 value = this.getValue(),
7966                 widget = this;
7968         // Rebuild the dropdown menu
7969         this.dropdownWidget.getMenu()
7970                 .clearItems()
7971                 .addItems( options.map( function ( opt ) {
7972                         var optValue = widget.cleanUpValue( opt.data );
7973                         return new OO.ui.MenuOptionWidget( {
7974                                 data: optValue,
7975                                 label: opt.label !== undefined ? opt.label : optValue
7976                         } );
7977                 } ) );
7979         // Restore the previous value, or reset to something sensible
7980         if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
7981                 // Previous value is still available, ensure consistency with the dropdown
7982                 this.setValue( value );
7983         } else {
7984                 // No longer valid, reset
7985                 if ( options.length ) {
7986                         this.setValue( options[ 0 ].data );
7987                 }
7988         }
7990         return this;
7994  * @inheritdoc
7995  */
7996 OO.ui.DropdownInputWidget.prototype.focus = function () {
7997         this.dropdownWidget.getMenu().toggle( true );
7998         return this;
8002  * @inheritdoc
8003  */
8004 OO.ui.DropdownInputWidget.prototype.blur = function () {
8005         this.dropdownWidget.getMenu().toggle( false );
8006         return this;
8010  * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
8011  * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
8012  * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
8013  * please see the [OOjs UI documentation on MediaWiki][1].
8015  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8017  *     @example
8018  *     // An example of selected, unselected, and disabled radio inputs
8019  *     var radio1 = new OO.ui.RadioInputWidget( {
8020  *         value: 'a',
8021  *         selected: true
8022  *     } );
8023  *     var radio2 = new OO.ui.RadioInputWidget( {
8024  *         value: 'b'
8025  *     } );
8026  *     var radio3 = new OO.ui.RadioInputWidget( {
8027  *         value: 'c',
8028  *         disabled: true
8029  *     } );
8030  *     // Create a fieldset layout with fields for each radio button.
8031  *     var fieldset = new OO.ui.FieldsetLayout( {
8032  *         label: 'Radio inputs'
8033  *     } );
8034  *     fieldset.addItems( [
8035  *         new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
8036  *         new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
8037  *         new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
8038  *     ] );
8039  *     $( 'body' ).append( fieldset.$element );
8041  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8043  * @class
8044  * @extends OO.ui.InputWidget
8046  * @constructor
8047  * @param {Object} [config] Configuration options
8048  * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
8049  */
8050 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
8051         // Configuration initialization
8052         config = config || {};
8054         // Parent constructor
8055         OO.ui.RadioInputWidget.parent.call( this, config );
8057         // Initialization
8058         this.$element
8059                 .addClass( 'oo-ui-radioInputWidget' )
8060                 // Required for pretty styling in MediaWiki theme
8061                 .append( $( '<span>' ) );
8062         this.setSelected( config.selected !== undefined ? config.selected : false );
8065 /* Setup */
8067 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
8069 /* Static Methods */
8072  * @inheritdoc
8073  */
8074 OO.ui.RadioInputWidget.static.gatherPreInfuseState = function ( node, config ) {
8075         var state = OO.ui.RadioInputWidget.parent.static.gatherPreInfuseState( node, config );
8076         state.checked = config.$input.prop( 'checked' );
8077         return state;
8080 /* Methods */
8083  * @inheritdoc
8084  * @protected
8085  */
8086 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
8087         return $( '<input>' ).attr( 'type', 'radio' );
8091  * @inheritdoc
8092  */
8093 OO.ui.RadioInputWidget.prototype.onEdit = function () {
8094         // RadioInputWidget doesn't track its state.
8098  * Set selection state of this radio button.
8100  * @param {boolean} state `true` for selected
8101  * @chainable
8102  */
8103 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
8104         // RadioInputWidget doesn't track its state.
8105         this.$input.prop( 'checked', state );
8106         return this;
8110  * Check if this radio button is selected.
8112  * @return {boolean} Radio is selected
8113  */
8114 OO.ui.RadioInputWidget.prototype.isSelected = function () {
8115         return this.$input.prop( 'checked' );
8119  * @inheritdoc
8120  */
8121 OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
8122         OO.ui.RadioInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
8123         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
8124                 this.setSelected( state.checked );
8125         }
8129  * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
8130  * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8131  * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8132  * more information about input widgets.
8134  * This and OO.ui.DropdownInputWidget support the same configuration options.
8136  *     @example
8137  *     // Example: A RadioSelectInputWidget with three options
8138  *     var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
8139  *         options: [
8140  *             { data: 'a', label: 'First' },
8141  *             { data: 'b', label: 'Second'},
8142  *             { data: 'c', label: 'Third' }
8143  *         ]
8144  *     } );
8145  *     $( 'body' ).append( radioSelectInput.$element );
8147  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8149  * @class
8150  * @extends OO.ui.InputWidget
8152  * @constructor
8153  * @param {Object} [config] Configuration options
8154  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8155  */
8156 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
8157         // Configuration initialization
8158         config = config || {};
8160         // Properties (must be done before parent constructor which calls #setDisabled)
8161         this.radioSelectWidget = new OO.ui.RadioSelectWidget();
8163         // Parent constructor
8164         OO.ui.RadioSelectInputWidget.parent.call( this, config );
8166         // Events
8167         this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
8169         // Initialization
8170         this.setOptions( config.options || [] );
8171         this.$element
8172                 .addClass( 'oo-ui-radioSelectInputWidget' )
8173                 .append( this.radioSelectWidget.$element );
8176 /* Setup */
8178 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
8180 /* Static Properties */
8182 OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
8184 /* Static Methods */
8187  * @inheritdoc
8188  */
8189 OO.ui.RadioSelectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
8190         var state = OO.ui.RadioSelectInputWidget.parent.static.gatherPreInfuseState( node, config );
8191         state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
8192         return state;
8196  * @inheritdoc
8197  */
8198 OO.ui.RadioSelectInputWidget.static.reusePreInfuseDOM = function ( node, config ) {
8199         config = OO.ui.RadioSelectInputWidget.parent.static.reusePreInfuseDOM( node, config );
8200         // Cannot reuse the `<input type=radio>` set
8201         delete config.$input;
8202         return config;
8205 /* Methods */
8208  * @inheritdoc
8209  * @protected
8210  */
8211 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
8212         return $( '<input>' ).attr( 'type', 'hidden' );
8216  * Handles menu select events.
8218  * @private
8219  * @param {OO.ui.RadioOptionWidget} item Selected menu item
8220  */
8221 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
8222         this.setValue( item.getData() );
8226  * @inheritdoc
8227  */
8228 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
8229         value = this.cleanUpValue( value );
8230         this.radioSelectWidget.selectItemByData( value );
8231         OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
8232         return this;
8236  * @inheritdoc
8237  */
8238 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
8239         this.radioSelectWidget.setDisabled( state );
8240         OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
8241         return this;
8245  * Set the options available for this input.
8247  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8248  * @chainable
8249  */
8250 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
8251         var
8252                 value = this.getValue(),
8253                 widget = this;
8255         // Rebuild the radioSelect menu
8256         this.radioSelectWidget
8257                 .clearItems()
8258                 .addItems( options.map( function ( opt ) {
8259                         var optValue = widget.cleanUpValue( opt.data );
8260                         return new OO.ui.RadioOptionWidget( {
8261                                 data: optValue,
8262                                 label: opt.label !== undefined ? opt.label : optValue
8263                         } );
8264                 } ) );
8266         // Restore the previous value, or reset to something sensible
8267         if ( this.radioSelectWidget.getItemFromData( value ) ) {
8268                 // Previous value is still available, ensure consistency with the radioSelect
8269                 this.setValue( value );
8270         } else {
8271                 // No longer valid, reset
8272                 if ( options.length ) {
8273                         this.setValue( options[ 0 ].data );
8274                 }
8275         }
8277         return this;
8281  * CheckboxMultiselectInputWidget is a
8282  * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
8283  * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
8284  * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
8285  * more information about input widgets.
8287  *     @example
8288  *     // Example: A CheckboxMultiselectInputWidget with three options
8289  *     var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
8290  *         options: [
8291  *             { data: 'a', label: 'First' },
8292  *             { data: 'b', label: 'Second'},
8293  *             { data: 'c', label: 'Third' }
8294  *         ]
8295  *     } );
8296  *     $( 'body' ).append( multiselectInput.$element );
8298  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8300  * @class
8301  * @extends OO.ui.InputWidget
8303  * @constructor
8304  * @param {Object} [config] Configuration options
8305  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8306  */
8307 OO.ui.CheckboxMultiselectInputWidget = function OoUiCheckboxMultiselectInputWidget( config ) {
8308         // Configuration initialization
8309         config = config || {};
8311         // Properties (must be done before parent constructor which calls #setDisabled)
8312         this.checkboxMultiselectWidget = new OO.ui.CheckboxMultiselectWidget();
8314         // Parent constructor
8315         OO.ui.CheckboxMultiselectInputWidget.parent.call( this, config );
8317         // Properties
8318         this.inputName = config.name;
8320         // Initialization
8321         this.$element
8322                 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
8323                 .append( this.checkboxMultiselectWidget.$element );
8324         // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
8325         this.$input.detach();
8326         this.setOptions( config.options || [] );
8327         // Have to repeat this from parent, as we need options to be set up for this to make sense
8328         this.setValue( config.value );
8331 /* Setup */
8333 OO.inheritClass( OO.ui.CheckboxMultiselectInputWidget, OO.ui.InputWidget );
8335 /* Static Properties */
8337 OO.ui.CheckboxMultiselectInputWidget.static.supportsSimpleLabel = false;
8339 /* Static Methods */
8342  * @inheritdoc
8343  */
8344 OO.ui.CheckboxMultiselectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
8345         var state = OO.ui.CheckboxMultiselectInputWidget.parent.static.gatherPreInfuseState( node, config );
8346         state.value = $( node ).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8347                 .toArray().map( function ( el ) { return el.value; } );
8348         return state;
8352  * @inheritdoc
8353  */
8354 OO.ui.CheckboxMultiselectInputWidget.static.reusePreInfuseDOM = function ( node, config ) {
8355         config = OO.ui.CheckboxMultiselectInputWidget.parent.static.reusePreInfuseDOM( node, config );
8356         // Cannot reuse the `<input type=checkbox>` set
8357         delete config.$input;
8358         return config;
8361 /* Methods */
8364  * @inheritdoc
8365  * @protected
8366  */
8367 OO.ui.CheckboxMultiselectInputWidget.prototype.getInputElement = function () {
8368         // Actually unused
8369         return $( '<div>' );
8373  * @inheritdoc
8374  */
8375 OO.ui.CheckboxMultiselectInputWidget.prototype.getValue = function () {
8376         var value = this.$element.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8377                 .toArray().map( function ( el ) { return el.value; } );
8378         if ( this.value !== value ) {
8379                 this.setValue( value );
8380         }
8381         return this.value;
8385  * @inheritdoc
8386  */
8387 OO.ui.CheckboxMultiselectInputWidget.prototype.setValue = function ( value ) {
8388         value = this.cleanUpValue( value );
8389         this.checkboxMultiselectWidget.selectItemsByData( value );
8390         OO.ui.CheckboxMultiselectInputWidget.parent.prototype.setValue.call( this, value );
8391         return this;
8395  * Clean up incoming value.
8397  * @param {string[]} value Original value
8398  * @return {string[]} Cleaned up value
8399  */
8400 OO.ui.CheckboxMultiselectInputWidget.prototype.cleanUpValue = function ( value ) {
8401         var i, singleValue,
8402                 cleanValue = [];
8403         if ( !Array.isArray( value ) ) {
8404                 return cleanValue;
8405         }
8406         for ( i = 0; i < value.length; i++ ) {
8407                 singleValue =
8408                         OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call( this, value[ i ] );
8409                 // Remove options that we don't have here
8410                 if ( !this.checkboxMultiselectWidget.getItemFromData( singleValue ) ) {
8411                         continue;
8412                 }
8413                 cleanValue.push( singleValue );
8414         }
8415         return cleanValue;
8419  * @inheritdoc
8420  */
8421 OO.ui.CheckboxMultiselectInputWidget.prototype.setDisabled = function ( state ) {
8422         this.checkboxMultiselectWidget.setDisabled( state );
8423         OO.ui.CheckboxMultiselectInputWidget.parent.prototype.setDisabled.call( this, state );
8424         return this;
8428  * Set the options available for this input.
8430  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8431  * @chainable
8432  */
8433 OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options ) {
8434         var widget = this;
8436         // Rebuild the checkboxMultiselectWidget menu
8437         this.checkboxMultiselectWidget
8438                 .clearItems()
8439                 .addItems( options.map( function ( opt ) {
8440                         var optValue, item;
8441                         optValue =
8442                                 OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call( widget, opt.data );
8443                         item = new OO.ui.CheckboxMultioptionWidget( {
8444                                 data: optValue,
8445                                 label: opt.label !== undefined ? opt.label : optValue
8446                         } );
8447                         // Set the 'name' and 'value' for form submission
8448                         item.checkbox.$input.attr( 'name', widget.inputName );
8449                         item.checkbox.setValue( optValue );
8450                         return item;
8451                 } ) );
8453         // Re-set the value, checking the checkboxes as needed.
8454         // This will also get rid of any stale options that we just removed.
8455         this.setValue( this.getValue() );
8457         return this;
8461  * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
8462  * size of the field as well as its presentation. In addition, these widgets can be configured
8463  * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
8464  * validation-pattern (used to determine if an input value is valid or not) and an input filter,
8465  * which modifies incoming values rather than validating them.
8466  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8468  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8470  *     @example
8471  *     // Example of a text input widget
8472  *     var textInput = new OO.ui.TextInputWidget( {
8473  *         value: 'Text input'
8474  *     } )
8475  *     $( 'body' ).append( textInput.$element );
8477  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8479  * @class
8480  * @extends OO.ui.InputWidget
8481  * @mixins OO.ui.mixin.IconElement
8482  * @mixins OO.ui.mixin.IndicatorElement
8483  * @mixins OO.ui.mixin.PendingElement
8484  * @mixins OO.ui.mixin.LabelElement
8486  * @constructor
8487  * @param {Object} [config] Configuration options
8488  * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
8489  *  'email', 'url', 'date' or 'number'. Ignored if `multiline` is true.
8491  *  Some values of `type` result in additional behaviors:
8493  *  - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
8494  *    empties the text field
8495  * @cfg {string} [placeholder] Placeholder text
8496  * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
8497  *  instruct the browser to focus this widget.
8498  * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
8499  * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
8500  * @cfg {boolean} [multiline=false] Allow multiple lines of text
8501  * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
8502  *  specifies minimum number of rows to display.
8503  * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
8504  *  Use the #maxRows config to specify a maximum number of displayed rows.
8505  * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
8506  *  Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
8507  * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
8508  *  the value or placeholder text: `'before'` or `'after'`
8509  * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
8510  * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
8511  * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
8512  *  pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
8513  *  (the value must contain only numbers); when RegExp, a regular expression that must match the
8514  *  value for it to be considered valid; when Function, a function receiving the value as parameter
8515  *  that must return true, or promise resolving to true, for it to be considered valid.
8516  */
8517 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
8518         // Configuration initialization
8519         config = $.extend( {
8520                 type: 'text',
8521                 labelPosition: 'after'
8522         }, config );
8523         if ( config.type === 'search' ) {
8524                 if ( config.icon === undefined ) {
8525                         config.icon = 'search';
8526                 }
8527                 // indicator: 'clear' is set dynamically later, depending on value
8528         }
8529         if ( config.required ) {
8530                 if ( config.indicator === undefined ) {
8531                         config.indicator = 'required';
8532                 }
8533         }
8535         // Parent constructor
8536         OO.ui.TextInputWidget.parent.call( this, config );
8538         // Mixin constructors
8539         OO.ui.mixin.IconElement.call( this, config );
8540         OO.ui.mixin.IndicatorElement.call( this, config );
8541         OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
8542         OO.ui.mixin.LabelElement.call( this, config );
8544         // Properties
8545         this.type = this.getSaneType( config );
8546         this.readOnly = false;
8547         this.multiline = !!config.multiline;
8548         this.autosize = !!config.autosize;
8549         this.minRows = config.rows !== undefined ? config.rows : '';
8550         this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
8551         this.validate = null;
8552         this.styleHeight = null;
8553         this.scrollWidth = null;
8555         // Clone for resizing
8556         if ( this.autosize ) {
8557                 this.$clone = this.$input
8558                         .clone()
8559                         .insertAfter( this.$input )
8560                         .attr( 'aria-hidden', 'true' )
8561                         .addClass( 'oo-ui-element-hidden' );
8562         }
8564         this.setValidation( config.validate );
8565         this.setLabelPosition( config.labelPosition );
8567         // Events
8568         this.$input.on( {
8569                 keypress: this.onKeyPress.bind( this ),
8570                 blur: this.onBlur.bind( this )
8571         } );
8572         this.$input.one( {
8573                 focus: this.onElementAttach.bind( this )
8574         } );
8575         this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
8576         this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
8577         this.on( 'labelChange', this.updatePosition.bind( this ) );
8578         this.connect( this, {
8579                 change: 'onChange',
8580                 disable: 'onDisable'
8581         } );
8583         // Initialization
8584         this.$element
8585                 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
8586                 .append( this.$icon, this.$indicator );
8587         this.setReadOnly( !!config.readOnly );
8588         this.updateSearchIndicator();
8589         if ( config.placeholder !== undefined ) {
8590                 this.$input.attr( 'placeholder', config.placeholder );
8591         }
8592         if ( config.maxLength !== undefined ) {
8593                 this.$input.attr( 'maxlength', config.maxLength );
8594         }
8595         if ( config.autofocus ) {
8596                 this.$input.attr( 'autofocus', 'autofocus' );
8597         }
8598         if ( config.required ) {
8599                 this.$input.attr( 'required', 'required' );
8600                 this.$input.attr( 'aria-required', 'true' );
8601         }
8602         if ( config.autocomplete === false ) {
8603                 this.$input.attr( 'autocomplete', 'off' );
8604                 // Turning off autocompletion also disables "form caching" when the user navigates to a
8605                 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
8606                 $( window ).on( {
8607                         beforeunload: function () {
8608                                 this.$input.removeAttr( 'autocomplete' );
8609                         }.bind( this ),
8610                         pageshow: function () {
8611                                 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
8612                                 // whole page... it shouldn't hurt, though.
8613                                 this.$input.attr( 'autocomplete', 'off' );
8614                         }.bind( this )
8615                 } );
8616         }
8617         if ( this.multiline && config.rows ) {
8618                 this.$input.attr( 'rows', config.rows );
8619         }
8620         if ( this.label || config.autosize ) {
8621                 this.installParentChangeDetector();
8622         }
8625 /* Setup */
8627 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
8628 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
8629 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
8630 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
8631 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
8633 /* Static Properties */
8635 OO.ui.TextInputWidget.static.validationPatterns = {
8636         'non-empty': /.+/,
8637         integer: /^\d+$/
8640 /* Static Methods */
8643  * @inheritdoc
8644  */
8645 OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
8646         var state = OO.ui.TextInputWidget.parent.static.gatherPreInfuseState( node, config );
8647         if ( config.multiline ) {
8648                 state.scrollTop = config.$input.scrollTop();
8649         }
8650         return state;
8653 /* Events */
8656  * An `enter` event is emitted when the user presses 'enter' inside the text box.
8658  * Not emitted if the input is multiline.
8660  * @event enter
8661  */
8664  * A `resize` event is emitted when autosize is set and the widget resizes
8666  * @event resize
8667  */
8669 /* Methods */
8672  * Handle icon mouse down events.
8674  * @private
8675  * @param {jQuery.Event} e Mouse down event
8676  */
8677 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
8678         if ( e.which === OO.ui.MouseButtons.LEFT ) {
8679                 this.$input[ 0 ].focus();
8680                 return false;
8681         }
8685  * Handle indicator mouse down events.
8687  * @private
8688  * @param {jQuery.Event} e Mouse down event
8689  */
8690 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
8691         if ( e.which === OO.ui.MouseButtons.LEFT ) {
8692                 if ( this.type === 'search' ) {
8693                         // Clear the text field
8694                         this.setValue( '' );
8695                 }
8696                 this.$input[ 0 ].focus();
8697                 return false;
8698         }
8702  * Handle key press events.
8704  * @private
8705  * @param {jQuery.Event} e Key press event
8706  * @fires enter If enter key is pressed and input is not multiline
8707  */
8708 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
8709         if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
8710                 this.emit( 'enter', e );
8711         }
8715  * Handle blur events.
8717  * @private
8718  * @param {jQuery.Event} e Blur event
8719  */
8720 OO.ui.TextInputWidget.prototype.onBlur = function () {
8721         this.setValidityFlag();
8725  * Handle element attach events.
8727  * @private
8728  * @param {jQuery.Event} e Element attach event
8729  */
8730 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
8731         // Any previously calculated size is now probably invalid if we reattached elsewhere
8732         this.valCache = null;
8733         this.adjustSize();
8734         this.positionLabel();
8738  * Handle change events.
8740  * @param {string} value
8741  * @private
8742  */
8743 OO.ui.TextInputWidget.prototype.onChange = function () {
8744         this.updateSearchIndicator();
8745         this.setValidityFlag();
8746         this.adjustSize();
8750  * Handle disable events.
8752  * @param {boolean} disabled Element is disabled
8753  * @private
8754  */
8755 OO.ui.TextInputWidget.prototype.onDisable = function () {
8756         this.updateSearchIndicator();
8760  * Check if the input is {@link #readOnly read-only}.
8762  * @return {boolean}
8763  */
8764 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
8765         return this.readOnly;
8769  * Set the {@link #readOnly read-only} state of the input.
8771  * @param {boolean} state Make input read-only
8772  * @chainable
8773  */
8774 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
8775         this.readOnly = !!state;
8776         this.$input.prop( 'readOnly', this.readOnly );
8777         this.updateSearchIndicator();
8778         return this;
8782  * Support function for making #onElementAttach work across browsers.
8784  * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
8785  * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
8787  * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
8788  * first time that the element gets attached to the documented.
8789  */
8790 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
8791         var mutationObserver, onRemove, topmostNode, fakeParentNode,
8792                 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
8793                 widget = this;
8795         if ( MutationObserver ) {
8796                 // The new way. If only it wasn't so ugly.
8798                 if ( this.$element.closest( 'html' ).length ) {
8799                         // Widget is attached already, do nothing. This breaks the functionality of this function when
8800                         // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
8801                         // would require observation of the whole document, which would hurt performance of other,
8802                         // more important code.
8803                         return;
8804                 }
8806                 // Find topmost node in the tree
8807                 topmostNode = this.$element[ 0 ];
8808                 while ( topmostNode.parentNode ) {
8809                         topmostNode = topmostNode.parentNode;
8810                 }
8812                 // We have no way to detect the $element being attached somewhere without observing the entire
8813                 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
8814                 // parent node of $element, and instead detect when $element is removed from it (and thus
8815                 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
8816                 // doesn't get attached, we end up back here and create the parent.
8818                 mutationObserver = new MutationObserver( function ( mutations ) {
8819                         var i, j, removedNodes;
8820                         for ( i = 0; i < mutations.length; i++ ) {
8821                                 removedNodes = mutations[ i ].removedNodes;
8822                                 for ( j = 0; j < removedNodes.length; j++ ) {
8823                                         if ( removedNodes[ j ] === topmostNode ) {
8824                                                 setTimeout( onRemove, 0 );
8825                                                 return;
8826                                         }
8827                                 }
8828                         }
8829                 } );
8831                 onRemove = function () {
8832                         // If the node was attached somewhere else, report it
8833                         if ( widget.$element.closest( 'html' ).length ) {
8834                                 widget.onElementAttach();
8835                         }
8836                         mutationObserver.disconnect();
8837                         widget.installParentChangeDetector();
8838                 };
8840                 // Create a fake parent and observe it
8841                 fakeParentNode = $( '<div>' ).append( topmostNode )[ 0 ];
8842                 mutationObserver.observe( fakeParentNode, { childList: true } );
8843         } else {
8844                 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
8845                 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
8846                 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
8847         }
8851  * Automatically adjust the size of the text input.
8853  * This only affects #multiline inputs that are {@link #autosize autosized}.
8855  * @chainable
8856  * @fires resize
8857  */
8858 OO.ui.TextInputWidget.prototype.adjustSize = function () {
8859         var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError,
8860                 idealHeight, newHeight, scrollWidth, property;
8862         if ( this.multiline && this.$input.val() !== this.valCache ) {
8863                 if ( this.autosize ) {
8864                         this.$clone
8865                                 .val( this.$input.val() )
8866                                 .attr( 'rows', this.minRows )
8867                                 // Set inline height property to 0 to measure scroll height
8868                                 .css( 'height', 0 );
8870                         this.$clone.removeClass( 'oo-ui-element-hidden' );
8872                         this.valCache = this.$input.val();
8874                         scrollHeight = this.$clone[ 0 ].scrollHeight;
8876                         // Remove inline height property to measure natural heights
8877                         this.$clone.css( 'height', '' );
8878                         innerHeight = this.$clone.innerHeight();
8879                         outerHeight = this.$clone.outerHeight();
8881                         // Measure max rows height
8882                         this.$clone
8883                                 .attr( 'rows', this.maxRows )
8884                                 .css( 'height', 'auto' )
8885                                 .val( '' );
8886                         maxInnerHeight = this.$clone.innerHeight();
8888                         // Difference between reported innerHeight and scrollHeight with no scrollbars present.
8889                         // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
8890                         measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
8891                         idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
8893                         this.$clone.addClass( 'oo-ui-element-hidden' );
8895                         // Only apply inline height when expansion beyond natural height is needed
8896                         // Use the difference between the inner and outer height as a buffer
8897                         newHeight = idealHeight > innerHeight ? idealHeight + ( outerHeight - innerHeight ) : '';
8898                         if ( newHeight !== this.styleHeight ) {
8899                                 this.$input.css( 'height', newHeight );
8900                                 this.styleHeight = newHeight;
8901                                 this.emit( 'resize' );
8902                         }
8903                 }
8904                 scrollWidth = this.$input[ 0 ].offsetWidth - this.$input[ 0 ].clientWidth;
8905                 if ( scrollWidth !== this.scrollWidth ) {
8906                         property = this.$element.css( 'direction' ) === 'rtl' ? 'left' : 'right';
8907                         // Reset
8908                         this.$label.css( { right: '', left: '' } );
8909                         this.$indicator.css( { right: '', left: '' } );
8911                         if ( scrollWidth ) {
8912                                 this.$indicator.css( property, scrollWidth );
8913                                 if ( this.labelPosition === 'after' ) {
8914                                         this.$label.css( property, scrollWidth );
8915                                 }
8916                         }
8918                         this.scrollWidth = scrollWidth;
8919                         this.positionLabel();
8920                 }
8921         }
8922         return this;
8926  * @inheritdoc
8927  * @protected
8928  */
8929 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
8930         if ( config.multiline ) {
8931                 return $( '<textarea>' );
8932         } else if ( this.getSaneType( config ) === 'number' ) {
8933                 return $( '<input>' )
8934                         .attr( 'step', 'any' )
8935                         .attr( 'type', 'number' );
8936         } else {
8937                 return $( '<input>' ).attr( 'type', this.getSaneType( config ) );
8938         }
8942  * Get sanitized value for 'type' for given config.
8944  * @param {Object} config Configuration options
8945  * @return {string|null}
8946  * @private
8947  */
8948 OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
8949         var allowedTypes = [
8950                 'text',
8951                 'password',
8952                 'search',
8953                 'email',
8954                 'url',
8955                 'date',
8956                 'number'
8957         ];
8958         return allowedTypes.indexOf( config.type ) !== -1 ? config.type : 'text';
8962  * Check if the input supports multiple lines.
8964  * @return {boolean}
8965  */
8966 OO.ui.TextInputWidget.prototype.isMultiline = function () {
8967         return !!this.multiline;
8971  * Check if the input automatically adjusts its size.
8973  * @return {boolean}
8974  */
8975 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
8976         return !!this.autosize;
8980  * Focus the input and select a specified range within the text.
8982  * @param {number} from Select from offset
8983  * @param {number} [to] Select to offset, defaults to from
8984  * @chainable
8985  */
8986 OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
8987         var isBackwards, start, end,
8988                 input = this.$input[ 0 ];
8990         to = to || from;
8992         isBackwards = to < from;
8993         start = isBackwards ? to : from;
8994         end = isBackwards ? from : to;
8996         this.focus();
8998         try {
8999                 input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
9000         } catch ( e ) {
9001                 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
9002                 // Rather than expensively check if the input is attached every time, just check
9003                 // if it was the cause of an error being thrown. If not, rethrow the error.
9004                 if ( this.getElementDocument().body.contains( input ) ) {
9005                         throw e;
9006                 }
9007         }
9008         return this;
9012  * Get an object describing the current selection range in a directional manner
9014  * @return {Object} Object containing 'from' and 'to' offsets
9015  */
9016 OO.ui.TextInputWidget.prototype.getRange = function () {
9017         var input = this.$input[ 0 ],
9018                 start = input.selectionStart,
9019                 end = input.selectionEnd,
9020                 isBackwards = input.selectionDirection === 'backward';
9022         return {
9023                 from: isBackwards ? end : start,
9024                 to: isBackwards ? start : end
9025         };
9029  * Get the length of the text input value.
9031  * This could differ from the length of #getValue if the
9032  * value gets filtered
9034  * @return {number} Input length
9035  */
9036 OO.ui.TextInputWidget.prototype.getInputLength = function () {
9037         return this.$input[ 0 ].value.length;
9041  * Focus the input and select the entire text.
9043  * @chainable
9044  */
9045 OO.ui.TextInputWidget.prototype.select = function () {
9046         return this.selectRange( 0, this.getInputLength() );
9050  * Focus the input and move the cursor to the start.
9052  * @chainable
9053  */
9054 OO.ui.TextInputWidget.prototype.moveCursorToStart = function () {
9055         return this.selectRange( 0 );
9059  * Focus the input and move the cursor to the end.
9061  * @chainable
9062  */
9063 OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
9064         return this.selectRange( this.getInputLength() );
9068  * Insert new content into the input.
9070  * @param {string} content Content to be inserted
9071  * @chainable
9072  */
9073 OO.ui.TextInputWidget.prototype.insertContent = function ( content ) {
9074         var start, end,
9075                 range = this.getRange(),
9076                 value = this.getValue();
9078         start = Math.min( range.from, range.to );
9079         end = Math.max( range.from, range.to );
9081         this.setValue( value.slice( 0, start ) + content + value.slice( end ) );
9082         this.selectRange( start + content.length );
9083         return this;
9087  * Insert new content either side of a selection.
9089  * @param {string} pre Content to be inserted before the selection
9090  * @param {string} post Content to be inserted after the selection
9091  * @chainable
9092  */
9093 OO.ui.TextInputWidget.prototype.encapsulateContent = function ( pre, post ) {
9094         var start, end,
9095                 range = this.getRange(),
9096                 offset = pre.length;
9098         start = Math.min( range.from, range.to );
9099         end = Math.max( range.from, range.to );
9101         this.selectRange( start ).insertContent( pre );
9102         this.selectRange( offset + end ).insertContent( post );
9104         this.selectRange( offset + start, offset + end );
9105         return this;
9109  * Set the validation pattern.
9111  * The validation pattern is either a regular expression, a function, or the symbolic name of a
9112  * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
9113  * value must contain only numbers).
9115  * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
9116  *  of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
9117  */
9118 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
9119         if ( validate instanceof RegExp || validate instanceof Function ) {
9120                 this.validate = validate;
9121         } else {
9122                 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
9123         }
9127  * Sets the 'invalid' flag appropriately.
9129  * @param {boolean} [isValid] Optionally override validation result
9130  */
9131 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
9132         var widget = this,
9133                 setFlag = function ( valid ) {
9134                         if ( !valid ) {
9135                                 widget.$input.attr( 'aria-invalid', 'true' );
9136                         } else {
9137                                 widget.$input.removeAttr( 'aria-invalid' );
9138                         }
9139                         widget.setFlags( { invalid: !valid } );
9140                 };
9142         if ( isValid !== undefined ) {
9143                 setFlag( isValid );
9144         } else {
9145                 this.getValidity().then( function () {
9146                         setFlag( true );
9147                 }, function () {
9148                         setFlag( false );
9149                 } );
9150         }
9154  * Check if a value is valid.
9156  * This method returns a promise that resolves with a boolean `true` if the current value is
9157  * considered valid according to the supplied {@link #validate validation pattern}.
9159  * @deprecated since v0.12.3
9160  * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
9161  */
9162 OO.ui.TextInputWidget.prototype.isValid = function () {
9163         var result;
9165         if ( this.validate instanceof Function ) {
9166                 result = this.validate( this.getValue() );
9167                 if ( result && $.isFunction( result.promise ) ) {
9168                         return result.promise();
9169                 } else {
9170                         return $.Deferred().resolve( !!result ).promise();
9171                 }
9172         } else {
9173                 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
9174         }
9178  * Get the validity of current value.
9180  * This method returns a promise that resolves if the value is valid and rejects if
9181  * it isn't. Uses the {@link #validate validation pattern}  to check for validity.
9183  * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
9184  */
9185 OO.ui.TextInputWidget.prototype.getValidity = function () {
9186         var result;
9188         function rejectOrResolve( valid ) {
9189                 if ( valid ) {
9190                         return $.Deferred().resolve().promise();
9191                 } else {
9192                         return $.Deferred().reject().promise();
9193                 }
9194         }
9196         if ( this.validate instanceof Function ) {
9197                 result = this.validate( this.getValue() );
9198                 if ( result && $.isFunction( result.promise ) ) {
9199                         return result.promise().then( function ( valid ) {
9200                                 return rejectOrResolve( valid );
9201                         } );
9202                 } else {
9203                         return rejectOrResolve( result );
9204                 }
9205         } else {
9206                 return rejectOrResolve( this.getValue().match( this.validate ) );
9207         }
9211  * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
9213  * @param {string} labelPosition Label position, 'before' or 'after'
9214  * @chainable
9215  */
9216 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
9217         this.labelPosition = labelPosition;
9218         if ( this.label ) {
9219                 // If there is no label and we only change the position, #updatePosition is a no-op,
9220                 // but it takes really a lot of work to do nothing.
9221                 this.updatePosition();
9222         }
9223         return this;
9227  * Update the position of the inline label.
9229  * This method is called by #setLabelPosition, and can also be called on its own if
9230  * something causes the label to be mispositioned.
9232  * @chainable
9233  */
9234 OO.ui.TextInputWidget.prototype.updatePosition = function () {
9235         var after = this.labelPosition === 'after';
9237         this.$element
9238                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
9239                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
9241         this.valCache = null;
9242         this.scrollWidth = null;
9243         this.adjustSize();
9244         this.positionLabel();
9246         return this;
9250  * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
9251  * already empty or when it's not editable.
9252  */
9253 OO.ui.TextInputWidget.prototype.updateSearchIndicator = function () {
9254         if ( this.type === 'search' ) {
9255                 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
9256                         this.setIndicator( null );
9257                 } else {
9258                         this.setIndicator( 'clear' );
9259                 }
9260         }
9264  * Position the label by setting the correct padding on the input.
9266  * @private
9267  * @chainable
9268  */
9269 OO.ui.TextInputWidget.prototype.positionLabel = function () {
9270         var after, rtl, property;
9271         // Clear old values
9272         this.$input
9273                 // Clear old values if present
9274                 .css( {
9275                         'padding-right': '',
9276                         'padding-left': ''
9277                 } );
9279         if ( this.label ) {
9280                 this.$element.append( this.$label );
9281         } else {
9282                 this.$label.detach();
9283                 return;
9284         }
9286         after = this.labelPosition === 'after';
9287         rtl = this.$element.css( 'direction' ) === 'rtl';
9288         property = after === rtl ? 'padding-left' : 'padding-right';
9290         this.$input.css( property, this.$label.outerWidth( true ) + ( after ? this.scrollWidth : 0 ) );
9292         return this;
9296  * @inheritdoc
9297  */
9298 OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
9299         OO.ui.TextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
9300         if ( state.scrollTop !== undefined ) {
9301                 this.$input.scrollTop( state.scrollTop );
9302         }
9306  * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
9307  * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
9308  * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
9310  * - by typing a value in the text input field. If the value exactly matches the value of a menu
9311  *   option, that option will appear to be selected.
9312  * - by choosing a value from the menu. The value of the chosen option will then appear in the text
9313  *   input field.
9315  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
9317  * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
9319  *     @example
9320  *     // Example: A ComboBoxInputWidget.
9321  *     var comboBox = new OO.ui.ComboBoxInputWidget( {
9322  *         label: 'ComboBoxInputWidget',
9323  *         value: 'Option 1',
9324  *         menu: {
9325  *             items: [
9326  *                 new OO.ui.MenuOptionWidget( {
9327  *                     data: 'Option 1',
9328  *                     label: 'Option One'
9329  *                 } ),
9330  *                 new OO.ui.MenuOptionWidget( {
9331  *                     data: 'Option 2',
9332  *                     label: 'Option Two'
9333  *                 } ),
9334  *                 new OO.ui.MenuOptionWidget( {
9335  *                     data: 'Option 3',
9336  *                     label: 'Option Three'
9337  *                 } ),
9338  *                 new OO.ui.MenuOptionWidget( {
9339  *                     data: 'Option 4',
9340  *                     label: 'Option Four'
9341  *                 } ),
9342  *                 new OO.ui.MenuOptionWidget( {
9343  *                     data: 'Option 5',
9344  *                     label: 'Option Five'
9345  *                 } )
9346  *             ]
9347  *         }
9348  *     } );
9349  *     $( 'body' ).append( comboBox.$element );
9351  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
9353  * @class
9354  * @extends OO.ui.TextInputWidget
9356  * @constructor
9357  * @param {Object} [config] Configuration options
9358  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9359  * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
9360  * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
9361  *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
9362  *  containing `<div>` and has a larger area. By default, the menu uses relative positioning.
9363  */
9364 OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
9365         // Configuration initialization
9366         config = $.extend( {
9367                 indicator: 'down',
9368                 autocomplete: false
9369         }, config );
9370         // For backwards-compatibility with ComboBoxWidget config
9371         $.extend( config, config.input );
9373         // Parent constructor
9374         OO.ui.ComboBoxInputWidget.parent.call( this, config );
9376         // Properties
9377         this.$overlay = config.$overlay || this.$element;
9378         this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
9379                 {
9380                         widget: this,
9381                         input: this,
9382                         $container: this.$element,
9383                         disabled: this.isDisabled()
9384                 },
9385                 config.menu
9386         ) );
9387         // For backwards-compatibility with ComboBoxWidget
9388         this.input = this;
9390         // Events
9391         this.$indicator.on( {
9392                 click: this.onIndicatorClick.bind( this ),
9393                 keypress: this.onIndicatorKeyPress.bind( this )
9394         } );
9395         this.connect( this, {
9396                 change: 'onInputChange',
9397                 enter: 'onInputEnter'
9398         } );
9399         this.menu.connect( this, {
9400                 choose: 'onMenuChoose',
9401                 add: 'onMenuItemsChange',
9402                 remove: 'onMenuItemsChange'
9403         } );
9405         // Initialization
9406         this.$input.attr( {
9407                 role: 'combobox',
9408                 'aria-autocomplete': 'list'
9409         } );
9410         // Do not override options set via config.menu.items
9411         if ( config.options !== undefined ) {
9412                 this.setOptions( config.options );
9413         }
9414         // Extra class for backwards-compatibility with ComboBoxWidget
9415         this.$element.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
9416         this.$overlay.append( this.menu.$element );
9417         this.onMenuItemsChange();
9420 /* Setup */
9422 OO.inheritClass( OO.ui.ComboBoxInputWidget, OO.ui.TextInputWidget );
9424 /* Methods */
9427  * Get the combobox's menu.
9429  * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
9430  */
9431 OO.ui.ComboBoxInputWidget.prototype.getMenu = function () {
9432         return this.menu;
9436  * Get the combobox's text input widget.
9438  * @return {OO.ui.TextInputWidget} Text input widget
9439  */
9440 OO.ui.ComboBoxInputWidget.prototype.getInput = function () {
9441         return this;
9445  * Handle input change events.
9447  * @private
9448  * @param {string} value New value
9449  */
9450 OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
9451         var match = this.menu.getItemFromData( value );
9453         this.menu.selectItem( match );
9454         if ( this.menu.getHighlightedItem() ) {
9455                 this.menu.highlightItem( match );
9456         }
9458         if ( !this.isDisabled() ) {
9459                 this.menu.toggle( true );
9460         }
9464  * Handle mouse click events.
9466  * @private
9467  * @param {jQuery.Event} e Mouse click event
9468  */
9469 OO.ui.ComboBoxInputWidget.prototype.onIndicatorClick = function ( e ) {
9470         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
9471                 this.menu.toggle();
9472                 this.$input[ 0 ].focus();
9473         }
9474         return false;
9478  * Handle key press events.
9480  * @private
9481  * @param {jQuery.Event} e Key press event
9482  */
9483 OO.ui.ComboBoxInputWidget.prototype.onIndicatorKeyPress = function ( e ) {
9484         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
9485                 this.menu.toggle();
9486                 this.$input[ 0 ].focus();
9487                 return false;
9488         }
9492  * Handle input enter events.
9494  * @private
9495  */
9496 OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
9497         if ( !this.isDisabled() ) {
9498                 this.menu.toggle( false );
9499         }
9503  * Handle menu choose events.
9505  * @private
9506  * @param {OO.ui.OptionWidget} item Chosen item
9507  */
9508 OO.ui.ComboBoxInputWidget.prototype.onMenuChoose = function ( item ) {
9509         this.setValue( item.getData() );
9513  * Handle menu item change events.
9515  * @private
9516  */
9517 OO.ui.ComboBoxInputWidget.prototype.onMenuItemsChange = function () {
9518         var match = this.menu.getItemFromData( this.getValue() );
9519         this.menu.selectItem( match );
9520         if ( this.menu.getHighlightedItem() ) {
9521                 this.menu.highlightItem( match );
9522         }
9523         this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu.isEmpty() );
9527  * @inheritdoc
9528  */
9529 OO.ui.ComboBoxInputWidget.prototype.setDisabled = function ( disabled ) {
9530         // Parent method
9531         OO.ui.ComboBoxInputWidget.parent.prototype.setDisabled.call( this, disabled );
9533         if ( this.menu ) {
9534                 this.menu.setDisabled( this.isDisabled() );
9535         }
9537         return this;
9541  * Set the options available for this input.
9543  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9544  * @chainable
9545  */
9546 OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
9547         this.getMenu()
9548                 .clearItems()
9549                 .addItems( options.map( function ( opt ) {
9550                         return new OO.ui.MenuOptionWidget( {
9551                                 data: opt.data,
9552                                 label: opt.label !== undefined ? opt.label : opt.data
9553                         } );
9554                 } ) );
9556         return this;
9560  * @class
9561  * @deprecated since 0.13.2; use OO.ui.ComboBoxInputWidget instead
9562  */
9563 OO.ui.ComboBoxWidget = OO.ui.ComboBoxInputWidget;
9566  * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
9567  * which is a widget that is specified by reference before any optional configuration settings.
9569  * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
9571  * - **left**: The label is placed before the field-widget and aligned with the left margin.
9572  *   A left-alignment is used for forms with many fields.
9573  * - **right**: The label is placed before the field-widget and aligned to the right margin.
9574  *   A right-alignment is used for long but familiar forms which users tab through,
9575  *   verifying the current field with a quick glance at the label.
9576  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9577  *   that users fill out from top to bottom.
9578  * - **inline**: The label is placed after the field-widget and aligned to the left.
9579  *   An inline-alignment is best used with checkboxes or radio buttons.
9581  * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
9582  * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
9584  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9586  * @class
9587  * @extends OO.ui.Layout
9588  * @mixins OO.ui.mixin.LabelElement
9589  * @mixins OO.ui.mixin.TitledElement
9591  * @constructor
9592  * @param {OO.ui.Widget} fieldWidget Field widget
9593  * @param {Object} [config] Configuration options
9594  * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
9595  * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
9596  *  The array may contain strings or OO.ui.HtmlSnippet instances.
9597  * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
9598  *  The array may contain strings or OO.ui.HtmlSnippet instances.
9599  * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
9600  *  in the upper-right corner of the rendered field; clicking it will display the text in a popup.
9601  *  For important messages, you are advised to use `notices`, as they are always shown.
9603  * @throws {Error} An error is thrown if no widget is specified
9604  */
9605 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
9606         var hasInputWidget, div;
9608         // Allow passing positional parameters inside the config object
9609         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
9610                 config = fieldWidget;
9611                 fieldWidget = config.fieldWidget;
9612         }
9614         // Make sure we have required constructor arguments
9615         if ( fieldWidget === undefined ) {
9616                 throw new Error( 'Widget not found' );
9617         }
9619         hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
9621         // Configuration initialization
9622         config = $.extend( { align: 'left' }, config );
9624         // Parent constructor
9625         OO.ui.FieldLayout.parent.call( this, config );
9627         // Mixin constructors
9628         OO.ui.mixin.LabelElement.call( this, config );
9629         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
9631         // Properties
9632         this.fieldWidget = fieldWidget;
9633         this.errors = [];
9634         this.notices = [];
9635         this.$field = $( '<div>' );
9636         this.$messages = $( '<ul>' );
9637         this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
9638         this.align = null;
9639         if ( config.help ) {
9640                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
9641                         classes: [ 'oo-ui-fieldLayout-help' ],
9642                         framed: false,
9643                         icon: 'info'
9644                 } );
9646                 div = $( '<div>' );
9647                 if ( config.help instanceof OO.ui.HtmlSnippet ) {
9648                         div.html( config.help.toString() );
9649                 } else {
9650                         div.text( config.help );
9651                 }
9652                 this.popupButtonWidget.getPopup().$body.append(
9653                         div.addClass( 'oo-ui-fieldLayout-help-content' )
9654                 );
9655                 this.$help = this.popupButtonWidget.$element;
9656         } else {
9657                 this.$help = $( [] );
9658         }
9660         // Events
9661         if ( hasInputWidget ) {
9662                 this.$label.on( 'click', this.onLabelClick.bind( this ) );
9663         }
9664         this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
9666         // Initialization
9667         this.$element
9668                 .addClass( 'oo-ui-fieldLayout' )
9669                 .append( this.$help, this.$body );
9670         this.$body.addClass( 'oo-ui-fieldLayout-body' );
9671         this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
9672         this.$field
9673                 .addClass( 'oo-ui-fieldLayout-field' )
9674                 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
9675                 .append( this.fieldWidget.$element );
9677         this.setErrors( config.errors || [] );
9678         this.setNotices( config.notices || [] );
9679         this.setAlignment( config.align );
9682 /* Setup */
9684 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
9685 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
9686 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.TitledElement );
9688 /* Methods */
9691  * Handle field disable events.
9693  * @private
9694  * @param {boolean} value Field is disabled
9695  */
9696 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
9697         this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
9701  * Handle label mouse click events.
9703  * @private
9704  * @param {jQuery.Event} e Mouse click event
9705  */
9706 OO.ui.FieldLayout.prototype.onLabelClick = function () {
9707         this.fieldWidget.simulateLabelClick();
9708         return false;
9712  * Get the widget contained by the field.
9714  * @return {OO.ui.Widget} Field widget
9715  */
9716 OO.ui.FieldLayout.prototype.getField = function () {
9717         return this.fieldWidget;
9721  * @protected
9722  * @param {string} kind 'error' or 'notice'
9723  * @param {string|OO.ui.HtmlSnippet} text
9724  * @return {jQuery}
9725  */
9726 OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
9727         var $listItem, $icon, message;
9728         $listItem = $( '<li>' );
9729         if ( kind === 'error' ) {
9730                 $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
9731         } else if ( kind === 'notice' ) {
9732                 $icon = new OO.ui.IconWidget( { icon: 'info' } ).$element;
9733         } else {
9734                 $icon = '';
9735         }
9736         message = new OO.ui.LabelWidget( { label: text } );
9737         $listItem
9738                 .append( $icon, message.$element )
9739                 .addClass( 'oo-ui-fieldLayout-messages-' + kind );
9740         return $listItem;
9744  * Set the field alignment mode.
9746  * @private
9747  * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
9748  * @chainable
9749  */
9750 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
9751         if ( value !== this.align ) {
9752                 // Default to 'left'
9753                 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
9754                         value = 'left';
9755                 }
9756                 // Reorder elements
9757                 if ( value === 'inline' ) {
9758                         this.$body.append( this.$field, this.$label );
9759                 } else {
9760                         this.$body.append( this.$label, this.$field );
9761                 }
9762                 // Set classes. The following classes can be used here:
9763                 // * oo-ui-fieldLayout-align-left
9764                 // * oo-ui-fieldLayout-align-right
9765                 // * oo-ui-fieldLayout-align-top
9766                 // * oo-ui-fieldLayout-align-inline
9767                 if ( this.align ) {
9768                         this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
9769                 }
9770                 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
9771                 this.align = value;
9772         }
9774         return this;
9778  * Set the list of error messages.
9780  * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
9781  *  The array may contain strings or OO.ui.HtmlSnippet instances.
9782  * @chainable
9783  */
9784 OO.ui.FieldLayout.prototype.setErrors = function ( errors ) {
9785         this.errors = errors.slice();
9786         this.updateMessages();
9787         return this;
9791  * Set the list of notice messages.
9793  * @param {Array} notices Notices about the widget, which will be displayed below the widget.
9794  *  The array may contain strings or OO.ui.HtmlSnippet instances.
9795  * @chainable
9796  */
9797 OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
9798         this.notices = notices.slice();
9799         this.updateMessages();
9800         return this;
9804  * Update the rendering of error and notice messages.
9806  * @private
9807  */
9808 OO.ui.FieldLayout.prototype.updateMessages = function () {
9809         var i;
9810         this.$messages.empty();
9812         if ( this.errors.length || this.notices.length ) {
9813                 this.$body.after( this.$messages );
9814         } else {
9815                 this.$messages.remove();
9816                 return;
9817         }
9819         for ( i = 0; i < this.notices.length; i++ ) {
9820                 this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
9821         }
9822         for ( i = 0; i < this.errors.length; i++ ) {
9823                 this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
9824         }
9828  * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
9829  * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
9830  * is required and is specified before any optional configuration settings.
9832  * Labels can be aligned in one of four ways:
9834  * - **left**: The label is placed before the field-widget and aligned with the left margin.
9835  *   A left-alignment is used for forms with many fields.
9836  * - **right**: The label is placed before the field-widget and aligned to the right margin.
9837  *   A right-alignment is used for long but familiar forms which users tab through,
9838  *   verifying the current field with a quick glance at the label.
9839  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9840  *   that users fill out from top to bottom.
9841  * - **inline**: The label is placed after the field-widget and aligned to the left.
9842  *   An inline-alignment is best used with checkboxes or radio buttons.
9844  * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
9845  * text is specified.
9847  *     @example
9848  *     // Example of an ActionFieldLayout
9849  *     var actionFieldLayout = new OO.ui.ActionFieldLayout(
9850  *         new OO.ui.TextInputWidget( {
9851  *             placeholder: 'Field widget'
9852  *         } ),
9853  *         new OO.ui.ButtonWidget( {
9854  *             label: 'Button'
9855  *         } ),
9856  *         {
9857  *             label: 'An ActionFieldLayout. This label is aligned top',
9858  *             align: 'top',
9859  *             help: 'This is help text'
9860  *         }
9861  *     );
9863  *     $( 'body' ).append( actionFieldLayout.$element );
9865  * @class
9866  * @extends OO.ui.FieldLayout
9868  * @constructor
9869  * @param {OO.ui.Widget} fieldWidget Field widget
9870  * @param {OO.ui.ButtonWidget} buttonWidget Button widget
9871  */
9872 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
9873         // Allow passing positional parameters inside the config object
9874         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
9875                 config = fieldWidget;
9876                 fieldWidget = config.fieldWidget;
9877                 buttonWidget = config.buttonWidget;
9878         }
9880         // Parent constructor
9881         OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
9883         // Properties
9884         this.buttonWidget = buttonWidget;
9885         this.$button = $( '<div>' );
9886         this.$input = $( '<div>' );
9888         // Initialization
9889         this.$element
9890                 .addClass( 'oo-ui-actionFieldLayout' );
9891         this.$button
9892                 .addClass( 'oo-ui-actionFieldLayout-button' )
9893                 .append( this.buttonWidget.$element );
9894         this.$input
9895                 .addClass( 'oo-ui-actionFieldLayout-input' )
9896                 .append( this.fieldWidget.$element );
9897         this.$field
9898                 .append( this.$input, this.$button );
9901 /* Setup */
9903 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
9906  * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
9907  * which each contain an individual widget and, optionally, a label. Each Fieldset can be
9908  * configured with a label as well. For more information and examples,
9909  * please see the [OOjs UI documentation on MediaWiki][1].
9911  *     @example
9912  *     // Example of a fieldset layout
9913  *     var input1 = new OO.ui.TextInputWidget( {
9914  *         placeholder: 'A text input field'
9915  *     } );
9917  *     var input2 = new OO.ui.TextInputWidget( {
9918  *         placeholder: 'A text input field'
9919  *     } );
9921  *     var fieldset = new OO.ui.FieldsetLayout( {
9922  *         label: 'Example of a fieldset layout'
9923  *     } );
9925  *     fieldset.addItems( [
9926  *         new OO.ui.FieldLayout( input1, {
9927  *             label: 'Field One'
9928  *         } ),
9929  *         new OO.ui.FieldLayout( input2, {
9930  *             label: 'Field Two'
9931  *         } )
9932  *     ] );
9933  *     $( 'body' ).append( fieldset.$element );
9935  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9937  * @class
9938  * @extends OO.ui.Layout
9939  * @mixins OO.ui.mixin.IconElement
9940  * @mixins OO.ui.mixin.LabelElement
9941  * @mixins OO.ui.mixin.GroupElement
9943  * @constructor
9944  * @param {Object} [config] Configuration options
9945  * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
9946  */
9947 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
9948         // Configuration initialization
9949         config = config || {};
9951         // Parent constructor
9952         OO.ui.FieldsetLayout.parent.call( this, config );
9954         // Mixin constructors
9955         OO.ui.mixin.IconElement.call( this, config );
9956         OO.ui.mixin.LabelElement.call( this, config );
9957         OO.ui.mixin.GroupElement.call( this, config );
9959         if ( config.help ) {
9960                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
9961                         classes: [ 'oo-ui-fieldsetLayout-help' ],
9962                         framed: false,
9963                         icon: 'info'
9964                 } );
9966                 this.popupButtonWidget.getPopup().$body.append(
9967                         $( '<div>' )
9968                                 .text( config.help )
9969                                 .addClass( 'oo-ui-fieldsetLayout-help-content' )
9970                 );
9971                 this.$help = this.popupButtonWidget.$element;
9972         } else {
9973                 this.$help = $( [] );
9974         }
9976         // Initialization
9977         this.$element
9978                 .addClass( 'oo-ui-fieldsetLayout' )
9979                 .prepend( this.$help, this.$icon, this.$label, this.$group );
9980         if ( Array.isArray( config.items ) ) {
9981                 this.addItems( config.items );
9982         }
9985 /* Setup */
9987 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
9988 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
9989 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
9990 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
9993  * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
9994  * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
9995  * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
9996  * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9998  * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
9999  * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
10000  * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
10001  * some fancier controls. Some controls have both regular and InputWidget variants, for example
10002  * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
10003  * often have simplified APIs to match the capabilities of HTML forms.
10004  * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
10006  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
10007  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10009  *     @example
10010  *     // Example of a form layout that wraps a fieldset layout
10011  *     var input1 = new OO.ui.TextInputWidget( {
10012  *         placeholder: 'Username'
10013  *     } );
10014  *     var input2 = new OO.ui.TextInputWidget( {
10015  *         placeholder: 'Password',
10016  *         type: 'password'
10017  *     } );
10018  *     var submit = new OO.ui.ButtonInputWidget( {
10019  *         label: 'Submit'
10020  *     } );
10022  *     var fieldset = new OO.ui.FieldsetLayout( {
10023  *         label: 'A form layout'
10024  *     } );
10025  *     fieldset.addItems( [
10026  *         new OO.ui.FieldLayout( input1, {
10027  *             label: 'Username',
10028  *             align: 'top'
10029  *         } ),
10030  *         new OO.ui.FieldLayout( input2, {
10031  *             label: 'Password',
10032  *             align: 'top'
10033  *         } ),
10034  *         new OO.ui.FieldLayout( submit )
10035  *     ] );
10036  *     var form = new OO.ui.FormLayout( {
10037  *         items: [ fieldset ],
10038  *         action: '/api/formhandler',
10039  *         method: 'get'
10040  *     } )
10041  *     $( 'body' ).append( form.$element );
10043  * @class
10044  * @extends OO.ui.Layout
10045  * @mixins OO.ui.mixin.GroupElement
10047  * @constructor
10048  * @param {Object} [config] Configuration options
10049  * @cfg {string} [method] HTML form `method` attribute
10050  * @cfg {string} [action] HTML form `action` attribute
10051  * @cfg {string} [enctype] HTML form `enctype` attribute
10052  * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
10053  */
10054 OO.ui.FormLayout = function OoUiFormLayout( config ) {
10055         var action;
10057         // Configuration initialization
10058         config = config || {};
10060         // Parent constructor
10061         OO.ui.FormLayout.parent.call( this, config );
10063         // Mixin constructors
10064         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10066         // Events
10067         this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
10069         // Make sure the action is safe
10070         action = config.action;
10071         if ( action !== undefined && !OO.ui.isSafeUrl( action ) ) {
10072                 action = './' + action;
10073         }
10075         // Initialization
10076         this.$element
10077                 .addClass( 'oo-ui-formLayout' )
10078                 .attr( {
10079                         method: config.method,
10080                         action: action,
10081                         enctype: config.enctype
10082                 } );
10083         if ( Array.isArray( config.items ) ) {
10084                 this.addItems( config.items );
10085         }
10088 /* Setup */
10090 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
10091 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
10093 /* Events */
10096  * A 'submit' event is emitted when the form is submitted.
10098  * @event submit
10099  */
10101 /* Static Properties */
10103 OO.ui.FormLayout.static.tagName = 'form';
10105 /* Methods */
10108  * Handle form submit events.
10110  * @private
10111  * @param {jQuery.Event} e Submit event
10112  * @fires submit
10113  */
10114 OO.ui.FormLayout.prototype.onFormSubmit = function () {
10115         if ( this.emit( 'submit' ) ) {
10116                 return false;
10117         }
10121  * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10122  * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10124  *     @example
10125  *     // Example of a panel layout
10126  *     var panel = new OO.ui.PanelLayout( {
10127  *         expanded: false,
10128  *         framed: true,
10129  *         padded: true,
10130  *         $content: $( '<p>A panel layout with padding and a frame.</p>' )
10131  *     } );
10132  *     $( 'body' ).append( panel.$element );
10134  * @class
10135  * @extends OO.ui.Layout
10137  * @constructor
10138  * @param {Object} [config] Configuration options
10139  * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10140  * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10141  * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10142  * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10143  */
10144 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
10145         // Configuration initialization
10146         config = $.extend( {
10147                 scrollable: false,
10148                 padded: false,
10149                 expanded: true,
10150                 framed: false
10151         }, config );
10153         // Parent constructor
10154         OO.ui.PanelLayout.parent.call( this, config );
10156         // Initialization
10157         this.$element.addClass( 'oo-ui-panelLayout' );
10158         if ( config.scrollable ) {
10159                 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
10160         }
10161         if ( config.padded ) {
10162                 this.$element.addClass( 'oo-ui-panelLayout-padded' );
10163         }
10164         if ( config.expanded ) {
10165                 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
10166         }
10167         if ( config.framed ) {
10168                 this.$element.addClass( 'oo-ui-panelLayout-framed' );
10169         }
10172 /* Setup */
10174 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
10176 /* Methods */
10179  * Focus the panel layout
10181  * The default implementation just focuses the first focusable element in the panel
10182  */
10183 OO.ui.PanelLayout.prototype.focus = function () {
10184         OO.ui.findFocusable( this.$element ).focus();
10188  * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
10189  * items), with small margins between them. Convenient when you need to put a number of block-level
10190  * widgets on a single line next to each other.
10192  * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
10194  *     @example
10195  *     // HorizontalLayout with a text input and a label
10196  *     var layout = new OO.ui.HorizontalLayout( {
10197  *       items: [
10198  *         new OO.ui.LabelWidget( { label: 'Label' } ),
10199  *         new OO.ui.TextInputWidget( { value: 'Text' } )
10200  *       ]
10201  *     } );
10202  *     $( 'body' ).append( layout.$element );
10204  * @class
10205  * @extends OO.ui.Layout
10206  * @mixins OO.ui.mixin.GroupElement
10208  * @constructor
10209  * @param {Object} [config] Configuration options
10210  * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
10211  */
10212 OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
10213         // Configuration initialization
10214         config = config || {};
10216         // Parent constructor
10217         OO.ui.HorizontalLayout.parent.call( this, config );
10219         // Mixin constructors
10220         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10222         // Initialization
10223         this.$element.addClass( 'oo-ui-horizontalLayout' );
10224         if ( Array.isArray( config.items ) ) {
10225                 this.addItems( config.items );
10226         }
10229 /* Setup */
10231 OO.inheritClass( OO.ui.HorizontalLayout, OO.ui.Layout );
10232 OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
10234 }( OO ) );