Fix namespace handling for uncategorized-categories-exceptionlist
[mediawiki.git] / resources / lib / oojs-ui / oojs-ui-core.js
blob872d81ff64e878cdd302920709c557a7025da732
1 /*!
2  * OOjs UI v0.18.3
3  * https://www.mediawiki.org/wiki/OOjs_UI
4  *
5  * Copyright 2011–2017 OOjs UI Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2017-01-04T00:22:40Z
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  * Puts a console warning with provided message.
266  * @param {string} message
267  */
268 OO.ui.warnDeprecation = function ( message ) {
269         if ( OO.getProp( window, 'console', 'warn' ) !== undefined ) {
270                 // eslint-disable-next-line no-console
271                 console.warn( message );
272         }
276  * Returns a function, that, when invoked, will only be triggered at most once
277  * during a given window of time. If called again during that window, it will
278  * wait until the window ends and then trigger itself again.
280  * As it's not knowable to the caller whether the function will actually run
281  * when the wrapper is called, return values from the function are entirely
282  * discarded.
284  * @param {Function} func
285  * @param {number} wait
286  * @return {Function}
287  */
288 OO.ui.throttle = function ( func, wait ) {
289         var context, args, timeout,
290                 previous = 0,
291                 run = function () {
292                         timeout = null;
293                         previous = OO.ui.now();
294                         func.apply( context, args );
295                 };
296         return function () {
297                 // Check how long it's been since the last time the function was
298                 // called, and whether it's more or less than the requested throttle
299                 // period. If it's less, run the function immediately. If it's more,
300                 // set a timeout for the remaining time -- but don't replace an
301                 // existing timeout, since that'd indefinitely prolong the wait.
302                 var remaining = wait - ( OO.ui.now() - previous );
303                 context = this;
304                 args = arguments;
305                 if ( remaining <= 0 ) {
306                         // Note: unless wait was ridiculously large, this means we'll
307                         // automatically run the first time the function was called in a
308                         // given period. (If you provide a wait period larger than the
309                         // current Unix timestamp, you *deserve* unexpected behavior.)
310                         clearTimeout( timeout );
311                         run();
312                 } else if ( !timeout ) {
313                         timeout = setTimeout( run, remaining );
314                 }
315         };
319  * A (possibly faster) way to get the current timestamp as an integer
321  * @return {number} Current timestamp
322  */
323 OO.ui.now = Date.now || function () {
324         return new Date().getTime();
328  * Reconstitute a JavaScript object corresponding to a widget created by
329  * the PHP implementation.
331  * This is an alias for `OO.ui.Element.static.infuse()`.
333  * @param {string|HTMLElement|jQuery} idOrNode
334  *   A DOM id (if a string) or node for the widget to infuse.
335  * @return {OO.ui.Element}
336  *   The `OO.ui.Element` corresponding to this (infusable) document node.
337  */
338 OO.ui.infuse = function ( idOrNode ) {
339         return OO.ui.Element.static.infuse( idOrNode );
342 ( function () {
343         /**
344          * Message store for the default implementation of OO.ui.msg
345          *
346          * Environments that provide a localization system should not use this, but should override
347          * OO.ui.msg altogether.
348          *
349          * @private
350          */
351         var messages = {
352                 // Tool tip for a button that moves items in a list down one place
353                 'ooui-outline-control-move-down': 'Move item down',
354                 // Tool tip for a button that moves items in a list up one place
355                 'ooui-outline-control-move-up': 'Move item up',
356                 // Tool tip for a button that removes items from a list
357                 'ooui-outline-control-remove': 'Remove item',
358                 // Label for the toolbar group that contains a list of all other available tools
359                 'ooui-toolbar-more': 'More',
360                 // Label for the fake tool that expands the full list of tools in a toolbar group
361                 'ooui-toolgroup-expand': 'More',
362                 // Label for the fake tool that collapses the full list of tools in a toolbar group
363                 'ooui-toolgroup-collapse': 'Fewer',
364                 // Default label for the accept button of a confirmation dialog
365                 'ooui-dialog-message-accept': 'OK',
366                 // Default label for the reject button of a confirmation dialog
367                 'ooui-dialog-message-reject': 'Cancel',
368                 // Title for process dialog error description
369                 'ooui-dialog-process-error': 'Something went wrong',
370                 // Label for process dialog dismiss error button, visible when describing errors
371                 'ooui-dialog-process-dismiss': 'Dismiss',
372                 // Label for process dialog retry action button, visible when describing only recoverable errors
373                 'ooui-dialog-process-retry': 'Try again',
374                 // Label for process dialog retry action button, visible when describing only warnings
375                 'ooui-dialog-process-continue': 'Continue',
376                 // Label for the file selection widget's select file button
377                 'ooui-selectfile-button-select': 'Select a file',
378                 // Label for the file selection widget if file selection is not supported
379                 'ooui-selectfile-not-supported': 'File selection is not supported',
380                 // Label for the file selection widget when no file is currently selected
381                 'ooui-selectfile-placeholder': 'No file is selected',
382                 // Label for the file selection widget's drop target
383                 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
384         };
386         /**
387          * Get a localized message.
388          *
389          * In environments that provide a localization system, this function should be overridden to
390          * return the message translated in the user's language. The default implementation always returns
391          * English messages.
392          *
393          * After the message key, message parameters may optionally be passed. In the default implementation,
394          * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
395          * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
396          * they support unnamed, ordered message parameters.
397          *
398          * @param {string} key Message key
399          * @param {...Mixed} [params] Message parameters
400          * @return {string} Translated message with parameters substituted
401          */
402         OO.ui.msg = function ( key ) {
403                 var message = messages[ key ],
404                         params = Array.prototype.slice.call( arguments, 1 );
405                 if ( typeof message === 'string' ) {
406                         // Perform $1 substitution
407                         message = message.replace( /\$(\d+)/g, function ( unused, n ) {
408                                 var i = parseInt( n, 10 );
409                                 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
410                         } );
411                 } else {
412                         // Return placeholder if message not found
413                         message = '[' + key + ']';
414                 }
415                 return message;
416         };
417 }() );
420  * Package a message and arguments for deferred resolution.
422  * Use this when you are statically specifying a message and the message may not yet be present.
424  * @param {string} key Message key
425  * @param {...Mixed} [params] Message parameters
426  * @return {Function} Function that returns the resolved message when executed
427  */
428 OO.ui.deferMsg = function () {
429         var args = arguments;
430         return function () {
431                 return OO.ui.msg.apply( OO.ui, args );
432         };
436  * Resolve a message.
438  * If the message is a function it will be executed, otherwise it will pass through directly.
440  * @param {Function|string} msg Deferred message, or message text
441  * @return {string} Resolved message
442  */
443 OO.ui.resolveMsg = function ( msg ) {
444         if ( $.isFunction( msg ) ) {
445                 return msg();
446         }
447         return msg;
451  * @param {string} url
452  * @return {boolean}
453  */
454 OO.ui.isSafeUrl = function ( url ) {
455         // Keep this function in sync with php/Tag.php
456         var i, protocolWhitelist;
458         function stringStartsWith( haystack, needle ) {
459                 return haystack.substr( 0, needle.length ) === needle;
460         }
462         protocolWhitelist = [
463                 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
464                 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
465                 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
466         ];
468         if ( url === '' ) {
469                 return true;
470         }
472         for ( i = 0; i < protocolWhitelist.length; i++ ) {
473                 if ( stringStartsWith( url, protocolWhitelist[ i ] + ':' ) ) {
474                         return true;
475                 }
476         }
478         // This matches '//' too
479         if ( stringStartsWith( url, '/' ) || stringStartsWith( url, './' ) ) {
480                 return true;
481         }
482         if ( stringStartsWith( url, '?' ) || stringStartsWith( url, '#' ) ) {
483                 return true;
484         }
486         return false;
490  * Check if the user has a 'mobile' device.
492  * For our purposes this means the user is primarily using an
493  * on-screen keyboard, touch input instead of a mouse and may
494  * have a physically small display.
496  * It is left up to implementors to decide how to compute this
497  * so the default implementation always returns false.
499  * @return {boolean} Use is on a mobile device
500  */
501 OO.ui.isMobile = function () {
502         return false;
506  * Mixin namespace.
507  */
510  * Namespace for OOjs UI mixins.
512  * Mixins are named according to the type of object they are intended to
513  * be mixed in to.  For example, OO.ui.mixin.GroupElement is intended to be
514  * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
515  * is intended to be mixed in to an instance of OO.ui.Widget.
517  * @class
518  * @singleton
519  */
520 OO.ui.mixin = {};
523  * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
524  * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
525  * connected to them and can't be interacted with.
527  * @abstract
528  * @class
530  * @constructor
531  * @param {Object} [config] Configuration options
532  * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
533  *  to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
534  *  for an example.
535  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
536  * @cfg {string} [id] The HTML id attribute used in the rendered tag.
537  * @cfg {string} [text] Text to insert
538  * @cfg {Array} [content] An array of content elements to append (after #text).
539  *  Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
540  *  Instances of OO.ui.Element will have their $element appended.
541  * @cfg {jQuery} [$content] Content elements to append (after #text).
542  * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
543  * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
544  *  Data can also be specified with the #setData method.
545  */
546 OO.ui.Element = function OoUiElement( config ) {
547         // Configuration initialization
548         config = config || {};
550         // Properties
551         this.$ = $;
552         this.visible = true;
553         this.data = config.data;
554         this.$element = config.$element ||
555                 $( document.createElement( this.getTagName() ) );
556         this.elementGroup = null;
557         this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses );
559         // Initialization
560         if ( Array.isArray( config.classes ) ) {
561                 this.$element.addClass( config.classes.join( ' ' ) );
562         }
563         if ( config.id ) {
564                 this.$element.attr( 'id', config.id );
565         }
566         if ( config.text ) {
567                 this.$element.text( config.text );
568         }
569         if ( config.content ) {
570                 // The `content` property treats plain strings as text; use an
571                 // HtmlSnippet to append HTML content.  `OO.ui.Element`s get their
572                 // appropriate $element appended.
573                 this.$element.append( config.content.map( function ( v ) {
574                         if ( typeof v === 'string' ) {
575                                 // Escape string so it is properly represented in HTML.
576                                 return document.createTextNode( v );
577                         } else if ( v instanceof OO.ui.HtmlSnippet ) {
578                                 // Bypass escaping.
579                                 return v.toString();
580                         } else if ( v instanceof OO.ui.Element ) {
581                                 return v.$element;
582                         }
583                         return v;
584                 } ) );
585         }
586         if ( config.$content ) {
587                 // The `$content` property treats plain strings as HTML.
588                 this.$element.append( config.$content );
589         }
592 /* Setup */
594 OO.initClass( OO.ui.Element );
596 /* Static Properties */
599  * The name of the HTML tag used by the element.
601  * The static value may be ignored if the #getTagName method is overridden.
603  * @static
604  * @inheritable
605  * @property {string}
606  */
607 OO.ui.Element.static.tagName = 'div';
609 /* Static Methods */
612  * Reconstitute a JavaScript object corresponding to a widget created
613  * by the PHP implementation.
615  * @param {string|HTMLElement|jQuery} idOrNode
616  *   A DOM id (if a string) or node for the widget to infuse.
617  * @return {OO.ui.Element}
618  *   The `OO.ui.Element` corresponding to this (infusable) document node.
619  *   For `Tag` objects emitted on the HTML side (used occasionally for content)
620  *   the value returned is a newly-created Element wrapping around the existing
621  *   DOM node.
622  */
623 OO.ui.Element.static.infuse = function ( idOrNode ) {
624         var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
625         // Verify that the type matches up.
626         // FIXME: uncomment after T89721 is fixed (see T90929)
627         /*
628         if ( !( obj instanceof this['class'] ) ) {
629                 throw new Error( 'Infusion type mismatch!' );
630         }
631         */
632         return obj;
636  * Implementation helper for `infuse`; skips the type check and has an
637  * extra property so that only the top-level invocation touches the DOM.
639  * @private
640  * @param {string|HTMLElement|jQuery} idOrNode
641  * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
642  *     when the top-level widget of this infusion is inserted into DOM,
643  *     replacing the original node; or false for top-level invocation.
644  * @return {OO.ui.Element}
645  */
646 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
647         // look for a cached result of a previous infusion.
648         var id, $elem, data, cls, parts, parent, obj, top, state, infusedChildren;
649         if ( typeof idOrNode === 'string' ) {
650                 id = idOrNode;
651                 $elem = $( document.getElementById( id ) );
652         } else {
653                 $elem = $( idOrNode );
654                 id = $elem.attr( 'id' );
655         }
656         if ( !$elem.length ) {
657                 throw new Error( 'Widget not found: ' + id );
658         }
659         if ( $elem[ 0 ].oouiInfused ) {
660                 $elem = $elem[ 0 ].oouiInfused;
661         }
662         data = $elem.data( 'ooui-infused' );
663         if ( data ) {
664                 // cached!
665                 if ( data === true ) {
666                         throw new Error( 'Circular dependency! ' + id );
667                 }
668                 if ( domPromise ) {
669                         // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
670                         state = data.constructor.static.gatherPreInfuseState( $elem, data );
671                         // restore dynamic state after the new element is re-inserted into DOM under infused parent
672                         domPromise.done( data.restorePreInfuseState.bind( data, state ) );
673                         infusedChildren = $elem.data( 'ooui-infused-children' );
674                         if ( infusedChildren && infusedChildren.length ) {
675                                 infusedChildren.forEach( function ( data ) {
676                                         var state = data.constructor.static.gatherPreInfuseState( $elem, data );
677                                         domPromise.done( data.restorePreInfuseState.bind( data, state ) );
678                                 } );
679                         }
680                 }
681                 return data;
682         }
683         data = $elem.attr( 'data-ooui' );
684         if ( !data ) {
685                 throw new Error( 'No infusion data found: ' + id );
686         }
687         try {
688                 data = $.parseJSON( data );
689         } catch ( _ ) {
690                 data = null;
691         }
692         if ( !( data && data._ ) ) {
693                 throw new Error( 'No valid infusion data found: ' + id );
694         }
695         if ( data._ === 'Tag' ) {
696                 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
697                 return new OO.ui.Element( { $element: $elem } );
698         }
699         parts = data._.split( '.' );
700         cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
701         if ( cls === undefined ) {
702                 // The PHP output might be old and not including the "OO.ui" prefix
703                 // TODO: Remove this back-compat after next major release
704                 cls = OO.getProp.apply( OO, [ OO.ui ].concat( parts ) );
705                 if ( cls === undefined ) {
706                         throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
707                 }
708         }
710         // Verify that we're creating an OO.ui.Element instance
711         parent = cls.parent;
713         while ( parent !== undefined ) {
714                 if ( parent === OO.ui.Element ) {
715                         // Safe
716                         break;
717                 }
719                 parent = parent.parent;
720         }
722         if ( parent !== OO.ui.Element ) {
723                 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
724         }
726         if ( domPromise === false ) {
727                 top = $.Deferred();
728                 domPromise = top.promise();
729         }
730         $elem.data( 'ooui-infused', true ); // prevent loops
731         data.id = id; // implicit
732         infusedChildren = [];
733         data = OO.copy( data, null, function deserialize( value ) {
734                 var infused;
735                 if ( OO.isPlainObject( value ) ) {
736                         if ( value.tag ) {
737                                 infused = OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
738                                 infusedChildren.push( infused );
739                                 // Flatten the structure
740                                 infusedChildren.push.apply( infusedChildren, infused.$element.data( 'ooui-infused-children' ) || [] );
741                                 infused.$element.removeData( 'ooui-infused-children' );
742                                 return infused;
743                         }
744                         if ( value.html !== undefined ) {
745                                 return new OO.ui.HtmlSnippet( value.html );
746                         }
747                 }
748         } );
749         // allow widgets to reuse parts of the DOM
750         data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data );
751         // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
752         state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
753         // rebuild widget
754         // eslint-disable-next-line new-cap
755         obj = new cls( data );
756         // now replace old DOM with this new DOM.
757         if ( top ) {
758                 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
759                 // so only mutate the DOM if we need to.
760                 if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
761                         $elem.replaceWith( obj.$element );
762                         // This element is now gone from the DOM, but if anyone is holding a reference to it,
763                         // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
764                         // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
765                         $elem[ 0 ].oouiInfused = obj.$element;
766                 }
767                 top.resolve();
768         }
769         obj.$element.data( 'ooui-infused', obj );
770         obj.$element.data( 'ooui-infused-children', infusedChildren );
771         // set the 'data-ooui' attribute so we can identify infused widgets
772         obj.$element.attr( 'data-ooui', '' );
773         // restore dynamic state after the new element is inserted into DOM
774         domPromise.done( obj.restorePreInfuseState.bind( obj, state ) );
775         return obj;
779  * Pick out parts of `node`'s DOM to be reused when infusing a widget.
781  * This method **must not** make any changes to the DOM, only find interesting pieces and add them
782  * to `config` (which should then be returned). Actual DOM juggling should then be done by the
783  * constructor, which will be given the enhanced config.
785  * @protected
786  * @param {HTMLElement} node
787  * @param {Object} config
788  * @return {Object}
789  */
790 OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) {
791         return config;
795  * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
796  * (and its children) that represent an Element of the same class and the given configuration,
797  * generated by the PHP implementation.
799  * This method is called just before `node` is detached from the DOM. The return value of this
800  * function will be passed to #restorePreInfuseState after the newly created widget's #$element
801  * is inserted into DOM to replace `node`.
803  * @protected
804  * @param {HTMLElement} node
805  * @param {Object} config
806  * @return {Object}
807  */
808 OO.ui.Element.static.gatherPreInfuseState = function () {
809         return {};
813  * Get a jQuery function within a specific document.
815  * @static
816  * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
817  * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
818  *   not in an iframe
819  * @return {Function} Bound jQuery function
820  */
821 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
822         function wrapper( selector ) {
823                 return $( selector, wrapper.context );
824         }
826         wrapper.context = this.getDocument( context );
828         if ( $iframe ) {
829                 wrapper.$iframe = $iframe;
830         }
832         return wrapper;
836  * Get the document of an element.
838  * @static
839  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
840  * @return {HTMLDocument|null} Document object
841  */
842 OO.ui.Element.static.getDocument = function ( obj ) {
843         // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
844         return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
845                 // Empty jQuery selections might have a context
846                 obj.context ||
847                 // HTMLElement
848                 obj.ownerDocument ||
849                 // Window
850                 obj.document ||
851                 // HTMLDocument
852                 ( obj.nodeType === 9 && obj ) ||
853                 null;
857  * Get the window of an element or document.
859  * @static
860  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
861  * @return {Window} Window object
862  */
863 OO.ui.Element.static.getWindow = function ( obj ) {
864         var doc = this.getDocument( obj );
865         return doc.defaultView;
869  * Get the direction of an element or document.
871  * @static
872  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
873  * @return {string} Text direction, either 'ltr' or 'rtl'
874  */
875 OO.ui.Element.static.getDir = function ( obj ) {
876         var isDoc, isWin;
878         if ( obj instanceof jQuery ) {
879                 obj = obj[ 0 ];
880         }
881         isDoc = obj.nodeType === 9;
882         isWin = obj.document !== undefined;
883         if ( isDoc || isWin ) {
884                 if ( isWin ) {
885                         obj = obj.document;
886                 }
887                 obj = obj.body;
888         }
889         return $( obj ).css( 'direction' );
893  * Get the offset between two frames.
895  * TODO: Make this function not use recursion.
897  * @static
898  * @param {Window} from Window of the child frame
899  * @param {Window} [to=window] Window of the parent frame
900  * @param {Object} [offset] Offset to start with, used internally
901  * @return {Object} Offset object, containing left and top properties
902  */
903 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
904         var i, len, frames, frame, rect;
906         if ( !to ) {
907                 to = window;
908         }
909         if ( !offset ) {
910                 offset = { top: 0, left: 0 };
911         }
912         if ( from.parent === from ) {
913                 return offset;
914         }
916         // Get iframe element
917         frames = from.parent.document.getElementsByTagName( 'iframe' );
918         for ( i = 0, len = frames.length; i < len; i++ ) {
919                 if ( frames[ i ].contentWindow === from ) {
920                         frame = frames[ i ];
921                         break;
922                 }
923         }
925         // Recursively accumulate offset values
926         if ( frame ) {
927                 rect = frame.getBoundingClientRect();
928                 offset.left += rect.left;
929                 offset.top += rect.top;
930                 if ( from !== to ) {
931                         this.getFrameOffset( from.parent, offset );
932                 }
933         }
934         return offset;
938  * Get the offset between two elements.
940  * The two elements may be in a different frame, but in that case the frame $element is in must
941  * be contained in the frame $anchor is in.
943  * @static
944  * @param {jQuery} $element Element whose position to get
945  * @param {jQuery} $anchor Element to get $element's position relative to
946  * @return {Object} Translated position coordinates, containing top and left properties
947  */
948 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
949         var iframe, iframePos,
950                 pos = $element.offset(),
951                 anchorPos = $anchor.offset(),
952                 elementDocument = this.getDocument( $element ),
953                 anchorDocument = this.getDocument( $anchor );
955         // If $element isn't in the same document as $anchor, traverse up
956         while ( elementDocument !== anchorDocument ) {
957                 iframe = elementDocument.defaultView.frameElement;
958                 if ( !iframe ) {
959                         throw new Error( '$element frame is not contained in $anchor frame' );
960                 }
961                 iframePos = $( iframe ).offset();
962                 pos.left += iframePos.left;
963                 pos.top += iframePos.top;
964                 elementDocument = iframe.ownerDocument;
965         }
966         pos.left -= anchorPos.left;
967         pos.top -= anchorPos.top;
968         return pos;
972  * Get element border sizes.
974  * @static
975  * @param {HTMLElement} el Element to measure
976  * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
977  */
978 OO.ui.Element.static.getBorders = function ( el ) {
979         var doc = el.ownerDocument,
980                 win = doc.defaultView,
981                 style = win.getComputedStyle( el, null ),
982                 $el = $( el ),
983                 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
984                 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
985                 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
986                 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
988         return {
989                 top: top,
990                 left: left,
991                 bottom: bottom,
992                 right: right
993         };
997  * Get dimensions of an element or window.
999  * @static
1000  * @param {HTMLElement|Window} el Element to measure
1001  * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1002  */
1003 OO.ui.Element.static.getDimensions = function ( el ) {
1004         var $el, $win,
1005                 doc = el.ownerDocument || el.document,
1006                 win = doc.defaultView;
1008         if ( win === el || el === doc.documentElement ) {
1009                 $win = $( win );
1010                 return {
1011                         borders: { top: 0, left: 0, bottom: 0, right: 0 },
1012                         scroll: {
1013                                 top: $win.scrollTop(),
1014                                 left: $win.scrollLeft()
1015                         },
1016                         scrollbar: { right: 0, bottom: 0 },
1017                         rect: {
1018                                 top: 0,
1019                                 left: 0,
1020                                 bottom: $win.innerHeight(),
1021                                 right: $win.innerWidth()
1022                         }
1023                 };
1024         } else {
1025                 $el = $( el );
1026                 return {
1027                         borders: this.getBorders( el ),
1028                         scroll: {
1029                                 top: $el.scrollTop(),
1030                                 left: $el.scrollLeft()
1031                         },
1032                         scrollbar: {
1033                                 right: $el.innerWidth() - el.clientWidth,
1034                                 bottom: $el.innerHeight() - el.clientHeight
1035                         },
1036                         rect: el.getBoundingClientRect()
1037                 };
1038         }
1042  * Get scrollable object parent
1044  * documentElement can't be used to get or set the scrollTop
1045  * property on Blink. Changing and testing its value lets us
1046  * use 'body' or 'documentElement' based on what is working.
1048  * https://code.google.com/p/chromium/issues/detail?id=303131
1050  * @static
1051  * @param {HTMLElement} el Element to find scrollable parent for
1052  * @return {HTMLElement} Scrollable parent
1053  */
1054 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1055         var scrollTop, body;
1057         if ( OO.ui.scrollableElement === undefined ) {
1058                 body = el.ownerDocument.body;
1059                 scrollTop = body.scrollTop;
1060                 body.scrollTop = 1;
1062                 if ( body.scrollTop === 1 ) {
1063                         body.scrollTop = scrollTop;
1064                         OO.ui.scrollableElement = 'body';
1065                 } else {
1066                         OO.ui.scrollableElement = 'documentElement';
1067                 }
1068         }
1070         return el.ownerDocument[ OO.ui.scrollableElement ];
1074  * Get closest scrollable container.
1076  * Traverses up until either a scrollable element or the root is reached, in which case the window
1077  * will be returned.
1079  * @static
1080  * @param {HTMLElement} el Element to find scrollable container for
1081  * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1082  * @return {HTMLElement} Closest scrollable container
1083  */
1084 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1085         var i, val,
1086                 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1087                 props = [ 'overflow-x', 'overflow-y' ],
1088                 $parent = $( el ).parent();
1090         if ( dimension === 'x' || dimension === 'y' ) {
1091                 props = [ 'overflow-' + dimension ];
1092         }
1094         while ( $parent.length ) {
1095                 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1096                         return $parent[ 0 ];
1097                 }
1098                 i = props.length;
1099                 while ( i-- ) {
1100                         val = $parent.css( props[ i ] );
1101                         if ( val === 'auto' || val === 'scroll' ) {
1102                                 return $parent[ 0 ];
1103                         }
1104                 }
1105                 $parent = $parent.parent();
1106         }
1107         return this.getDocument( el ).body;
1111  * Scroll element into view.
1113  * @static
1114  * @param {HTMLElement} el Element to scroll into view
1115  * @param {Object} [config] Configuration options
1116  * @param {string} [config.duration='fast'] jQuery animation duration value
1117  * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1118  *  to scroll in both directions
1119  * @param {Function} [config.complete] Function to call when scrolling completes.
1120  *  Deprecated since 0.15.4, use the return promise instead.
1121  * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1122  */
1123 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1124         var position, animations, callback, container, $container, elementDimensions, containerDimensions, $window,
1125                 deferred = $.Deferred();
1127         // Configuration initialization
1128         config = config || {};
1130         animations = {};
1131         callback = typeof config.complete === 'function' && config.complete;
1132         container = this.getClosestScrollableContainer( el, config.direction );
1133         $container = $( container );
1134         elementDimensions = this.getDimensions( el );
1135         containerDimensions = this.getDimensions( container );
1136         $window = $( this.getWindow( el ) );
1138         // Compute the element's position relative to the container
1139         if ( $container.is( 'html, body' ) ) {
1140                 // If the scrollable container is the root, this is easy
1141                 position = {
1142                         top: elementDimensions.rect.top,
1143                         bottom: $window.innerHeight() - elementDimensions.rect.bottom,
1144                         left: elementDimensions.rect.left,
1145                         right: $window.innerWidth() - elementDimensions.rect.right
1146                 };
1147         } else {
1148                 // Otherwise, we have to subtract el's coordinates from container's coordinates
1149                 position = {
1150                         top: elementDimensions.rect.top - ( containerDimensions.rect.top + containerDimensions.borders.top ),
1151                         bottom: containerDimensions.rect.bottom - containerDimensions.borders.bottom - containerDimensions.scrollbar.bottom - elementDimensions.rect.bottom,
1152                         left: elementDimensions.rect.left - ( containerDimensions.rect.left + containerDimensions.borders.left ),
1153                         right: containerDimensions.rect.right - containerDimensions.borders.right - containerDimensions.scrollbar.right - elementDimensions.rect.right
1154                 };
1155         }
1157         if ( !config.direction || config.direction === 'y' ) {
1158                 if ( position.top < 0 ) {
1159                         animations.scrollTop = containerDimensions.scroll.top + position.top;
1160                 } else if ( position.top > 0 && position.bottom < 0 ) {
1161                         animations.scrollTop = containerDimensions.scroll.top + Math.min( position.top, -position.bottom );
1162                 }
1163         }
1164         if ( !config.direction || config.direction === 'x' ) {
1165                 if ( position.left < 0 ) {
1166                         animations.scrollLeft = containerDimensions.scroll.left + position.left;
1167                 } else if ( position.left > 0 && position.right < 0 ) {
1168                         animations.scrollLeft = containerDimensions.scroll.left + Math.min( position.left, -position.right );
1169                 }
1170         }
1171         if ( !$.isEmptyObject( animations ) ) {
1172                 $container.stop( true ).animate( animations, config.duration === undefined ? 'fast' : config.duration );
1173                 $container.queue( function ( next ) {
1174                         if ( callback ) {
1175                                 callback();
1176                         }
1177                         deferred.resolve();
1178                         next();
1179                 } );
1180         } else {
1181                 if ( callback ) {
1182                         callback();
1183                 }
1184                 deferred.resolve();
1185         }
1186         return deferred.promise();
1190  * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1191  * and reserve space for them, because it probably doesn't.
1193  * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1194  * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1195  * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1196  * and then reattach (or show) them back.
1198  * @static
1199  * @param {HTMLElement} el Element to reconsider the scrollbars on
1200  */
1201 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1202         var i, len, scrollLeft, scrollTop, nodes = [];
1203         // Save scroll position
1204         scrollLeft = el.scrollLeft;
1205         scrollTop = el.scrollTop;
1206         // Detach all children
1207         while ( el.firstChild ) {
1208                 nodes.push( el.firstChild );
1209                 el.removeChild( el.firstChild );
1210         }
1211         // Force reflow
1212         void el.offsetHeight;
1213         // Reattach all children
1214         for ( i = 0, len = nodes.length; i < len; i++ ) {
1215                 el.appendChild( nodes[ i ] );
1216         }
1217         // Restore scroll position (no-op if scrollbars disappeared)
1218         el.scrollLeft = scrollLeft;
1219         el.scrollTop = scrollTop;
1222 /* Methods */
1225  * Toggle visibility of an element.
1227  * @param {boolean} [show] Make element visible, omit to toggle visibility
1228  * @fires visible
1229  * @chainable
1230  */
1231 OO.ui.Element.prototype.toggle = function ( show ) {
1232         show = show === undefined ? !this.visible : !!show;
1234         if ( show !== this.isVisible() ) {
1235                 this.visible = show;
1236                 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1237                 this.emit( 'toggle', show );
1238         }
1240         return this;
1244  * Check if element is visible.
1246  * @return {boolean} element is visible
1247  */
1248 OO.ui.Element.prototype.isVisible = function () {
1249         return this.visible;
1253  * Get element data.
1255  * @return {Mixed} Element data
1256  */
1257 OO.ui.Element.prototype.getData = function () {
1258         return this.data;
1262  * Set element data.
1264  * @param {Mixed} data Element data
1265  * @chainable
1266  */
1267 OO.ui.Element.prototype.setData = function ( data ) {
1268         this.data = data;
1269         return this;
1273  * Check if element supports one or more methods.
1275  * @param {string|string[]} methods Method or list of methods to check
1276  * @return {boolean} All methods are supported
1277  */
1278 OO.ui.Element.prototype.supports = function ( methods ) {
1279         var i, len,
1280                 support = 0;
1282         methods = Array.isArray( methods ) ? methods : [ methods ];
1283         for ( i = 0, len = methods.length; i < len; i++ ) {
1284                 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1285                         support++;
1286                 }
1287         }
1289         return methods.length === support;
1293  * Update the theme-provided classes.
1295  * @localdoc This is called in element mixins and widget classes any time state changes.
1296  *   Updating is debounced, minimizing overhead of changing multiple attributes and
1297  *   guaranteeing that theme updates do not occur within an element's constructor
1298  */
1299 OO.ui.Element.prototype.updateThemeClasses = function () {
1300         this.debouncedUpdateThemeClassesHandler();
1304  * @private
1305  * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1306  *   make them synchronous.
1307  */
1308 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1309         OO.ui.theme.updateElementClasses( this );
1313  * Get the HTML tag name.
1315  * Override this method to base the result on instance information.
1317  * @return {string} HTML tag name
1318  */
1319 OO.ui.Element.prototype.getTagName = function () {
1320         return this.constructor.static.tagName;
1324  * Check if the element is attached to the DOM
1326  * @return {boolean} The element is attached to the DOM
1327  */
1328 OO.ui.Element.prototype.isElementAttached = function () {
1329         return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1333  * Get the DOM document.
1335  * @return {HTMLDocument} Document object
1336  */
1337 OO.ui.Element.prototype.getElementDocument = function () {
1338         // Don't cache this in other ways either because subclasses could can change this.$element
1339         return OO.ui.Element.static.getDocument( this.$element );
1343  * Get the DOM window.
1345  * @return {Window} Window object
1346  */
1347 OO.ui.Element.prototype.getElementWindow = function () {
1348         return OO.ui.Element.static.getWindow( this.$element );
1352  * Get closest scrollable container.
1354  * @return {HTMLElement} Closest scrollable container
1355  */
1356 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1357         return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1361  * Get group element is in.
1363  * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1364  */
1365 OO.ui.Element.prototype.getElementGroup = function () {
1366         return this.elementGroup;
1370  * Set group element is in.
1372  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1373  * @chainable
1374  */
1375 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1376         this.elementGroup = group;
1377         return this;
1381  * Scroll element into view.
1383  * @param {Object} [config] Configuration options
1384  * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1385  */
1386 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1387         if (
1388                 !this.isElementAttached() ||
1389                 !this.isVisible() ||
1390                 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1391         ) {
1392                 return $.Deferred().resolve();
1393         }
1394         return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1398  * Restore the pre-infusion dynamic state for this widget.
1400  * This method is called after #$element has been inserted into DOM. The parameter is the return
1401  * value of #gatherPreInfuseState.
1403  * @protected
1404  * @param {Object} state
1405  */
1406 OO.ui.Element.prototype.restorePreInfuseState = function () {
1410  * Wraps an HTML snippet for use with configuration values which default
1411  * to strings.  This bypasses the default html-escaping done to string
1412  * values.
1414  * @class
1416  * @constructor
1417  * @param {string} [content] HTML content
1418  */
1419 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
1420         // Properties
1421         this.content = content;
1424 /* Setup */
1426 OO.initClass( OO.ui.HtmlSnippet );
1428 /* Methods */
1431  * Render into HTML.
1433  * @return {string} Unchanged HTML snippet.
1434  */
1435 OO.ui.HtmlSnippet.prototype.toString = function () {
1436         return this.content;
1440  * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1441  * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1442  * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1443  * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1444  * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1446  * @abstract
1447  * @class
1448  * @extends OO.ui.Element
1449  * @mixins OO.EventEmitter
1451  * @constructor
1452  * @param {Object} [config] Configuration options
1453  */
1454 OO.ui.Layout = function OoUiLayout( config ) {
1455         // Configuration initialization
1456         config = config || {};
1458         // Parent constructor
1459         OO.ui.Layout.parent.call( this, config );
1461         // Mixin constructors
1462         OO.EventEmitter.call( this );
1464         // Initialization
1465         this.$element.addClass( 'oo-ui-layout' );
1468 /* Setup */
1470 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1471 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1474  * Widgets are compositions of one or more OOjs UI elements that users can both view
1475  * and interact with. All widgets can be configured and modified via a standard API,
1476  * and their state can change dynamically according to a model.
1478  * @abstract
1479  * @class
1480  * @extends OO.ui.Element
1481  * @mixins OO.EventEmitter
1483  * @constructor
1484  * @param {Object} [config] Configuration options
1485  * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1486  *  appearance reflects this state.
1487  */
1488 OO.ui.Widget = function OoUiWidget( config ) {
1489         // Initialize config
1490         config = $.extend( { disabled: false }, config );
1492         // Parent constructor
1493         OO.ui.Widget.parent.call( this, config );
1495         // Mixin constructors
1496         OO.EventEmitter.call( this );
1498         // Properties
1499         this.disabled = null;
1500         this.wasDisabled = null;
1502         // Initialization
1503         this.$element.addClass( 'oo-ui-widget' );
1504         this.setDisabled( !!config.disabled );
1507 /* Setup */
1509 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1510 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1512 /* Static Properties */
1515  * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1516  * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1517  * handling.
1519  * @static
1520  * @inheritable
1521  * @property {boolean}
1522  */
1523 OO.ui.Widget.static.supportsSimpleLabel = false;
1525 /* Events */
1528  * @event disable
1530  * A 'disable' event is emitted when the disabled state of the widget changes
1531  * (i.e. on disable **and** enable).
1533  * @param {boolean} disabled Widget is disabled
1534  */
1537  * @event toggle
1539  * A 'toggle' event is emitted when the visibility of the widget changes.
1541  * @param {boolean} visible Widget is visible
1542  */
1544 /* Methods */
1547  * Check if the widget is disabled.
1549  * @return {boolean} Widget is disabled
1550  */
1551 OO.ui.Widget.prototype.isDisabled = function () {
1552         return this.disabled;
1556  * Set the 'disabled' state of the widget.
1558  * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1560  * @param {boolean} disabled Disable widget
1561  * @chainable
1562  */
1563 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1564         var isDisabled;
1566         this.disabled = !!disabled;
1567         isDisabled = this.isDisabled();
1568         if ( isDisabled !== this.wasDisabled ) {
1569                 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1570                 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1571                 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1572                 this.emit( 'disable', isDisabled );
1573                 this.updateThemeClasses();
1574         }
1575         this.wasDisabled = isDisabled;
1577         return this;
1581  * Update the disabled state, in case of changes in parent widget.
1583  * @chainable
1584  */
1585 OO.ui.Widget.prototype.updateDisabled = function () {
1586         this.setDisabled( this.disabled );
1587         return this;
1591  * Theme logic.
1593  * @abstract
1594  * @class
1596  * @constructor
1597  */
1598 OO.ui.Theme = function OoUiTheme() {};
1600 /* Setup */
1602 OO.initClass( OO.ui.Theme );
1604 /* Methods */
1607  * Get a list of classes to be applied to a widget.
1609  * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1610  * otherwise state transitions will not work properly.
1612  * @param {OO.ui.Element} element Element for which to get classes
1613  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1614  */
1615 OO.ui.Theme.prototype.getElementClasses = function () {
1616         return { on: [], off: [] };
1620  * Update CSS classes provided by the theme.
1622  * For elements with theme logic hooks, this should be called any time there's a state change.
1624  * @param {OO.ui.Element} element Element for which to update classes
1625  */
1626 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
1627         var $elements = $( [] ),
1628                 classes = this.getElementClasses( element );
1630         if ( element.$icon ) {
1631                 $elements = $elements.add( element.$icon );
1632         }
1633         if ( element.$indicator ) {
1634                 $elements = $elements.add( element.$indicator );
1635         }
1637         $elements
1638                 .removeClass( classes.off.join( ' ' ) )
1639                 .addClass( classes.on.join( ' ' ) );
1643  * Get the transition duration in milliseconds for dialogs opening/closing
1645  * The dialog should be fully rendered this many milliseconds after the
1646  * ready process has executed.
1648  * @return {number} Transition duration in milliseconds
1649  */
1650 OO.ui.Theme.prototype.getDialogTransitionDuration = function () {
1651         return 0;
1655  * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1656  * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1657  * order in which users will navigate through the focusable elements via the "tab" key.
1659  *     @example
1660  *     // TabIndexedElement is mixed into the ButtonWidget class
1661  *     // to provide a tabIndex property.
1662  *     var button1 = new OO.ui.ButtonWidget( {
1663  *         label: 'fourth',
1664  *         tabIndex: 4
1665  *     } );
1666  *     var button2 = new OO.ui.ButtonWidget( {
1667  *         label: 'second',
1668  *         tabIndex: 2
1669  *     } );
1670  *     var button3 = new OO.ui.ButtonWidget( {
1671  *         label: 'third',
1672  *         tabIndex: 3
1673  *     } );
1674  *     var button4 = new OO.ui.ButtonWidget( {
1675  *         label: 'first',
1676  *         tabIndex: 1
1677  *     } );
1678  *     $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1680  * @abstract
1681  * @class
1683  * @constructor
1684  * @param {Object} [config] Configuration options
1685  * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1686  *  the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1687  *  functionality will be applied to it instead.
1688  * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1689  *  order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1690  *  to remove the element from the tab-navigation flow.
1691  */
1692 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
1693         // Configuration initialization
1694         config = $.extend( { tabIndex: 0 }, config );
1696         // Properties
1697         this.$tabIndexed = null;
1698         this.tabIndex = null;
1700         // Events
1701         this.connect( this, { disable: 'onTabIndexedElementDisable' } );
1703         // Initialization
1704         this.setTabIndex( config.tabIndex );
1705         this.setTabIndexedElement( config.$tabIndexed || this.$element );
1708 /* Setup */
1710 OO.initClass( OO.ui.mixin.TabIndexedElement );
1712 /* Methods */
1715  * Set the element that should use the tabindex functionality.
1717  * This method is used to retarget a tabindex mixin so that its functionality applies
1718  * to the specified element. If an element is currently using the functionality, the mixin’s
1719  * effect on that element is removed before the new element is set up.
1721  * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1722  * @chainable
1723  */
1724 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
1725         var tabIndex = this.tabIndex;
1726         // Remove attributes from old $tabIndexed
1727         this.setTabIndex( null );
1728         // Force update of new $tabIndexed
1729         this.$tabIndexed = $tabIndexed;
1730         this.tabIndex = tabIndex;
1731         return this.updateTabIndex();
1735  * Set the value of the tabindex.
1737  * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
1738  * @chainable
1739  */
1740 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
1741         tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
1743         if ( this.tabIndex !== tabIndex ) {
1744                 this.tabIndex = tabIndex;
1745                 this.updateTabIndex();
1746         }
1748         return this;
1752  * Update the `tabindex` attribute, in case of changes to tab index or
1753  * disabled state.
1755  * @private
1756  * @chainable
1757  */
1758 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
1759         if ( this.$tabIndexed ) {
1760                 if ( this.tabIndex !== null ) {
1761                         // Do not index over disabled elements
1762                         this.$tabIndexed.attr( {
1763                                 tabindex: this.isDisabled() ? -1 : this.tabIndex,
1764                                 // Support: ChromeVox and NVDA
1765                                 // These do not seem to inherit aria-disabled from parent elements
1766                                 'aria-disabled': this.isDisabled().toString()
1767                         } );
1768                 } else {
1769                         this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
1770                 }
1771         }
1772         return this;
1776  * Handle disable events.
1778  * @private
1779  * @param {boolean} disabled Element is disabled
1780  */
1781 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
1782         this.updateTabIndex();
1786  * Get the value of the tabindex.
1788  * @return {number|null} Tabindex value
1789  */
1790 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
1791         return this.tabIndex;
1795  * ButtonElement is often mixed into other classes to generate a button, which is a clickable
1796  * interface element that can be configured with access keys for accessibility.
1797  * See the [OOjs UI documentation on MediaWiki] [1] for examples.
1799  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
1801  * @abstract
1802  * @class
1804  * @constructor
1805  * @param {Object} [config] Configuration options
1806  * @cfg {jQuery} [$button] The button element created by the class.
1807  *  If this configuration is omitted, the button element will use a generated `<a>`.
1808  * @cfg {boolean} [framed=true] Render the button with a frame
1809  */
1810 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
1811         // Configuration initialization
1812         config = config || {};
1814         // Properties
1815         this.$button = null;
1816         this.framed = null;
1817         this.active = config.active !== undefined && config.active;
1818         this.onMouseUpHandler = this.onMouseUp.bind( this );
1819         this.onMouseDownHandler = this.onMouseDown.bind( this );
1820         this.onKeyDownHandler = this.onKeyDown.bind( this );
1821         this.onKeyUpHandler = this.onKeyUp.bind( this );
1822         this.onClickHandler = this.onClick.bind( this );
1823         this.onKeyPressHandler = this.onKeyPress.bind( this );
1825         // Initialization
1826         this.$element.addClass( 'oo-ui-buttonElement' );
1827         this.toggleFramed( config.framed === undefined || config.framed );
1828         this.setButtonElement( config.$button || $( '<a>' ) );
1831 /* Setup */
1833 OO.initClass( OO.ui.mixin.ButtonElement );
1835 /* Static Properties */
1838  * Cancel mouse down events.
1840  * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
1841  * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
1842  * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
1843  * parent widget.
1845  * @static
1846  * @inheritable
1847  * @property {boolean}
1848  */
1849 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
1851 /* Events */
1854  * A 'click' event is emitted when the button element is clicked.
1856  * @event click
1857  */
1859 /* Methods */
1862  * Set the button element.
1864  * This method is used to retarget a button mixin so that its functionality applies to
1865  * the specified button element instead of the one created by the class. If a button element
1866  * is already set, the method will remove the mixin’s effect on that element.
1868  * @param {jQuery} $button Element to use as button
1869  */
1870 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
1871         if ( this.$button ) {
1872                 this.$button
1873                         .removeClass( 'oo-ui-buttonElement-button' )
1874                         .removeAttr( 'role accesskey' )
1875                         .off( {
1876                                 mousedown: this.onMouseDownHandler,
1877                                 keydown: this.onKeyDownHandler,
1878                                 click: this.onClickHandler,
1879                                 keypress: this.onKeyPressHandler
1880                         } );
1881         }
1883         this.$button = $button
1884                 .addClass( 'oo-ui-buttonElement-button' )
1885                 .on( {
1886                         mousedown: this.onMouseDownHandler,
1887                         keydown: this.onKeyDownHandler,
1888                         click: this.onClickHandler,
1889                         keypress: this.onKeyPressHandler
1890                 } );
1892         // Add `role="button"` on `<a>` elements, where it's needed
1893         // `toUppercase()` is added for XHTML documents
1894         if ( this.$button.prop( 'tagName' ).toUpperCase() === 'A' ) {
1895                 this.$button.attr( 'role', 'button' );
1896         }
1900  * Handles mouse down events.
1902  * @protected
1903  * @param {jQuery.Event} e Mouse down event
1904  */
1905 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
1906         if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
1907                 return;
1908         }
1909         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
1910         // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
1911         // reliably remove the pressed class
1912         this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
1913         // Prevent change of focus unless specifically configured otherwise
1914         if ( this.constructor.static.cancelButtonMouseDownEvents ) {
1915                 return false;
1916         }
1920  * Handles mouse up events.
1922  * @protected
1923  * @param {MouseEvent} e Mouse up event
1924  */
1925 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
1926         if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
1927                 return;
1928         }
1929         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
1930         // Stop listening for mouseup, since we only needed this once
1931         this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
1935  * Handles mouse click events.
1937  * @protected
1938  * @param {jQuery.Event} e Mouse click event
1939  * @fires click
1940  */
1941 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
1942         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
1943                 if ( this.emit( 'click' ) ) {
1944                         return false;
1945                 }
1946         }
1950  * Handles key down events.
1952  * @protected
1953  * @param {jQuery.Event} e Key down event
1954  */
1955 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
1956         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
1957                 return;
1958         }
1959         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
1960         // Run the keyup handler no matter where the key is when the button is let go, so we can
1961         // reliably remove the pressed class
1962         this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
1966  * Handles key up events.
1968  * @protected
1969  * @param {KeyboardEvent} e Key up event
1970  */
1971 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
1972         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
1973                 return;
1974         }
1975         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
1976         // Stop listening for keyup, since we only needed this once
1977         this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
1981  * Handles key press events.
1983  * @protected
1984  * @param {jQuery.Event} e Key press event
1985  * @fires click
1986  */
1987 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
1988         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
1989                 if ( this.emit( 'click' ) ) {
1990                         return false;
1991                 }
1992         }
1996  * Check if button has a frame.
1998  * @return {boolean} Button is framed
1999  */
2000 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
2001         return this.framed;
2005  * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2007  * @param {boolean} [framed] Make button framed, omit to toggle
2008  * @chainable
2009  */
2010 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
2011         framed = framed === undefined ? !this.framed : !!framed;
2012         if ( framed !== this.framed ) {
2013                 this.framed = framed;
2014                 this.$element
2015                         .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
2016                         .toggleClass( 'oo-ui-buttonElement-framed', framed );
2017                 this.updateThemeClasses();
2018         }
2020         return this;
2024  * Set the button's active state.
2026  * The active state can be set on:
2028  *  - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2029  *  - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2030  *  - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2032  * @protected
2033  * @param {boolean} value Make button active
2034  * @chainable
2035  */
2036 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
2037         this.active = !!value;
2038         this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
2039         this.updateThemeClasses();
2040         return this;
2044  * Check if the button is active
2046  * @protected
2047  * @return {boolean} The button is active
2048  */
2049 OO.ui.mixin.ButtonElement.prototype.isActive = function () {
2050         return this.active;
2054  * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2055  * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2056  * items from the group is done through the interface the class provides.
2057  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2059  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
2061  * @abstract
2062  * @class
2064  * @constructor
2065  * @param {Object} [config] Configuration options
2066  * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2067  *  is omitted, the group element will use a generated `<div>`.
2068  */
2069 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
2070         // Configuration initialization
2071         config = config || {};
2073         // Properties
2074         this.$group = null;
2075         this.items = [];
2076         this.aggregateItemEvents = {};
2078         // Initialization
2079         this.setGroupElement( config.$group || $( '<div>' ) );
2082 /* Events */
2085  * @event change
2087  * A change event is emitted when the set of selected items changes.
2089  * @param {OO.ui.Element[]} items Items currently in the group
2090  */
2092 /* Methods */
2095  * Set the group element.
2097  * If an element is already set, items will be moved to the new element.
2099  * @param {jQuery} $group Element to use as group
2100  */
2101 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
2102         var i, len;
2104         this.$group = $group;
2105         for ( i = 0, len = this.items.length; i < len; i++ ) {
2106                 this.$group.append( this.items[ i ].$element );
2107         }
2111  * Check if a group contains no items.
2113  * @return {boolean} Group is empty
2114  */
2115 OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
2116         return !this.items.length;
2120  * Get all items in the group.
2122  * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
2123  * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
2124  * from a group).
2126  * @return {OO.ui.Element[]} An array of items.
2127  */
2128 OO.ui.mixin.GroupElement.prototype.getItems = function () {
2129         return this.items.slice( 0 );
2133  * Get an item by its data.
2135  * Only the first item with matching data will be returned. To return all matching items,
2136  * use the #getItemsFromData method.
2138  * @param {Object} data Item data to search for
2139  * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2140  */
2141 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
2142         var i, len, item,
2143                 hash = OO.getHash( data );
2145         for ( i = 0, len = this.items.length; i < len; i++ ) {
2146                 item = this.items[ i ];
2147                 if ( hash === OO.getHash( item.getData() ) ) {
2148                         return item;
2149                 }
2150         }
2152         return null;
2156  * Get items by their data.
2158  * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2160  * @param {Object} data Item data to search for
2161  * @return {OO.ui.Element[]} Items with equivalent data
2162  */
2163 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
2164         var i, len, item,
2165                 hash = OO.getHash( data ),
2166                 items = [];
2168         for ( i = 0, len = this.items.length; i < len; i++ ) {
2169                 item = this.items[ i ];
2170                 if ( hash === OO.getHash( item.getData() ) ) {
2171                         items.push( item );
2172                 }
2173         }
2175         return items;
2179  * Aggregate the events emitted by the group.
2181  * When events are aggregated, the group will listen to all contained items for the event,
2182  * and then emit the event under a new name. The new event will contain an additional leading
2183  * parameter containing the item that emitted the original event. Other arguments emitted from
2184  * the original event are passed through.
2186  * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
2187  *  aggregated  (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
2188  *  A `null` value will remove aggregated events.
2190  * @throws {Error} An error is thrown if aggregation already exists.
2191  */
2192 OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
2193         var i, len, item, add, remove, itemEvent, groupEvent;
2195         for ( itemEvent in events ) {
2196                 groupEvent = events[ itemEvent ];
2198                 // Remove existing aggregated event
2199                 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
2200                         // Don't allow duplicate aggregations
2201                         if ( groupEvent ) {
2202                                 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
2203                         }
2204                         // Remove event aggregation from existing items
2205                         for ( i = 0, len = this.items.length; i < len; i++ ) {
2206                                 item = this.items[ i ];
2207                                 if ( item.connect && item.disconnect ) {
2208                                         remove = {};
2209                                         remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2210                                         item.disconnect( this, remove );
2211                                 }
2212                         }
2213                         // Prevent future items from aggregating event
2214                         delete this.aggregateItemEvents[ itemEvent ];
2215                 }
2217                 // Add new aggregate event
2218                 if ( groupEvent ) {
2219                         // Make future items aggregate event
2220                         this.aggregateItemEvents[ itemEvent ] = groupEvent;
2221                         // Add event aggregation to existing items
2222                         for ( i = 0, len = this.items.length; i < len; i++ ) {
2223                                 item = this.items[ i ];
2224                                 if ( item.connect && item.disconnect ) {
2225                                         add = {};
2226                                         add[ itemEvent ] = [ 'emit', groupEvent, item ];
2227                                         item.connect( this, add );
2228                                 }
2229                         }
2230                 }
2231         }
2235  * Add items to the group.
2237  * Items will be added to the end of the group array unless the optional `index` parameter specifies
2238  * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2240  * @param {OO.ui.Element[]} items An array of items to add to the group
2241  * @param {number} [index] Index of the insertion point
2242  * @chainable
2243  */
2244 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
2245         var i, len, item, itemEvent, events, currentIndex,
2246                 itemElements = [];
2248         for ( i = 0, len = items.length; i < len; i++ ) {
2249                 item = items[ i ];
2251                 // Check if item exists then remove it first, effectively "moving" it
2252                 currentIndex = this.items.indexOf( item );
2253                 if ( currentIndex >= 0 ) {
2254                         this.removeItems( [ item ] );
2255                         // Adjust index to compensate for removal
2256                         if ( currentIndex < index ) {
2257                                 index--;
2258                         }
2259                 }
2260                 // Add the item
2261                 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
2262                         events = {};
2263                         for ( itemEvent in this.aggregateItemEvents ) {
2264                                 events[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2265                         }
2266                         item.connect( this, events );
2267                 }
2268                 item.setElementGroup( this );
2269                 itemElements.push( item.$element.get( 0 ) );
2270         }
2272         if ( index === undefined || index < 0 || index >= this.items.length ) {
2273                 this.$group.append( itemElements );
2274                 this.items.push.apply( this.items, items );
2275         } else if ( index === 0 ) {
2276                 this.$group.prepend( itemElements );
2277                 this.items.unshift.apply( this.items, items );
2278         } else {
2279                 this.items[ index ].$element.before( itemElements );
2280                 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
2281         }
2283         this.emit( 'change', this.getItems() );
2284         return this;
2288  * Remove the specified items from a group.
2290  * Removed items are detached (not removed) from the DOM so that they may be reused.
2291  * To remove all items from a group, you may wish to use the #clearItems method instead.
2293  * @param {OO.ui.Element[]} items An array of items to remove
2294  * @chainable
2295  */
2296 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
2297         var i, len, item, index, events, itemEvent;
2299         // Remove specific items
2300         for ( i = 0, len = items.length; i < len; i++ ) {
2301                 item = items[ i ];
2302                 index = this.items.indexOf( item );
2303                 if ( index !== -1 ) {
2304                         if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
2305                                 events = {};
2306                                 for ( itemEvent in this.aggregateItemEvents ) {
2307                                         events[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2308                                 }
2309                                 item.disconnect( this, events );
2310                         }
2311                         item.setElementGroup( null );
2312                         this.items.splice( index, 1 );
2313                         item.$element.detach();
2314                 }
2315         }
2317         this.emit( 'change', this.getItems() );
2318         return this;
2322  * Clear all items from the group.
2324  * Cleared items are detached from the DOM, not removed, so that they may be reused.
2325  * To remove only a subset of items from a group, use the #removeItems method.
2327  * @chainable
2328  */
2329 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
2330         var i, len, item, remove, itemEvent;
2332         // Remove all items
2333         for ( i = 0, len = this.items.length; i < len; i++ ) {
2334                 item = this.items[ i ];
2335                 if (
2336                         item.connect && item.disconnect &&
2337                         !$.isEmptyObject( this.aggregateItemEvents )
2338                 ) {
2339                         remove = {};
2340                         if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
2341                                 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2342                         }
2343                         item.disconnect( this, remove );
2344                 }
2345                 item.setElementGroup( null );
2346                 item.$element.detach();
2347         }
2349         this.emit( 'change', this.getItems() );
2350         this.items = [];
2351         return this;
2355  * IconElement is often mixed into other classes to generate an icon.
2356  * Icons are graphics, about the size of normal text. They are used to aid the user
2357  * in locating a control or to convey information in a space-efficient way. See the
2358  * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2359  * included in the library.
2361  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2363  * @abstract
2364  * @class
2366  * @constructor
2367  * @param {Object} [config] Configuration options
2368  * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2369  *  the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2370  *  the icon element be set to an existing icon instead of the one generated by this class, set a
2371  *  value using a jQuery selection. For example:
2373  *      // Use a <div> tag instead of a <span>
2374  *     $icon: $("<div>")
2375  *     // Use an existing icon element instead of the one generated by the class
2376  *     $icon: this.$element
2377  *     // Use an icon element from a child widget
2378  *     $icon: this.childwidget.$element
2379  * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2380  *  symbolic names.  A map is used for i18n purposes and contains a `default` icon
2381  *  name and additional names keyed by language code. The `default` name is used when no icon is keyed
2382  *  by the user's language.
2384  *  Example of an i18n map:
2386  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2387  *  See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2388  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2389  * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2390  *  text. The icon title is displayed when users move the mouse over the icon.
2391  */
2392 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
2393         // Configuration initialization
2394         config = config || {};
2396         // Properties
2397         this.$icon = null;
2398         this.icon = null;
2399         this.iconTitle = null;
2401         // Initialization
2402         this.setIcon( config.icon || this.constructor.static.icon );
2403         this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
2404         this.setIconElement( config.$icon || $( '<span>' ) );
2407 /* Setup */
2409 OO.initClass( OO.ui.mixin.IconElement );
2411 /* Static Properties */
2414  * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2415  * for i18n purposes and contains a `default` icon name and additional names keyed by
2416  * language code. The `default` name is used when no icon is keyed by the user's language.
2418  * Example of an i18n map:
2420  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2422  * Note: the static property will be overridden if the #icon configuration is used.
2424  * @static
2425  * @inheritable
2426  * @property {Object|string}
2427  */
2428 OO.ui.mixin.IconElement.static.icon = null;
2431  * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2432  * function that returns title text, or `null` for no title.
2434  * The static property will be overridden if the #iconTitle configuration is used.
2436  * @static
2437  * @inheritable
2438  * @property {string|Function|null}
2439  */
2440 OO.ui.mixin.IconElement.static.iconTitle = null;
2442 /* Methods */
2445  * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2446  * applies to the specified icon element instead of the one created by the class. If an icon
2447  * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2448  * and mixin methods will no longer affect the element.
2450  * @param {jQuery} $icon Element to use as icon
2451  */
2452 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
2453         if ( this.$icon ) {
2454                 this.$icon
2455                         .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
2456                         .removeAttr( 'title' );
2457         }
2459         this.$icon = $icon
2460                 .addClass( 'oo-ui-iconElement-icon' )
2461                 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
2462         if ( this.iconTitle !== null ) {
2463                 this.$icon.attr( 'title', this.iconTitle );
2464         }
2466         this.updateThemeClasses();
2470  * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2471  * The icon parameter can also be set to a map of icon names. See the #icon config setting
2472  * for an example.
2474  * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2475  *  by language code, or `null` to remove the icon.
2476  * @chainable
2477  */
2478 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
2479         icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
2480         icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
2482         if ( this.icon !== icon ) {
2483                 if ( this.$icon ) {
2484                         if ( this.icon !== null ) {
2485                                 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
2486                         }
2487                         if ( icon !== null ) {
2488                                 this.$icon.addClass( 'oo-ui-icon-' + icon );
2489                         }
2490                 }
2491                 this.icon = icon;
2492         }
2494         this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
2495         this.updateThemeClasses();
2497         return this;
2501  * Set the icon title. Use `null` to remove the title.
2503  * @param {string|Function|null} iconTitle A text string used as the icon title,
2504  *  a function that returns title text, or `null` for no title.
2505  * @chainable
2506  */
2507 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
2508         iconTitle = typeof iconTitle === 'function' ||
2509                 ( typeof iconTitle === 'string' && iconTitle.length ) ?
2510                         OO.ui.resolveMsg( iconTitle ) : null;
2512         if ( this.iconTitle !== iconTitle ) {
2513                 this.iconTitle = iconTitle;
2514                 if ( this.$icon ) {
2515                         if ( this.iconTitle !== null ) {
2516                                 this.$icon.attr( 'title', iconTitle );
2517                         } else {
2518                                 this.$icon.removeAttr( 'title' );
2519                         }
2520                 }
2521         }
2523         return this;
2527  * Get the symbolic name of the icon.
2529  * @return {string} Icon name
2530  */
2531 OO.ui.mixin.IconElement.prototype.getIcon = function () {
2532         return this.icon;
2536  * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2538  * @return {string} Icon title text
2539  */
2540 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
2541         return this.iconTitle;
2545  * IndicatorElement is often mixed into other classes to generate an indicator.
2546  * Indicators are small graphics that are generally used in two ways:
2548  * - To draw attention to the status of an item. For example, an indicator might be
2549  *   used to show that an item in a list has errors that need to be resolved.
2550  * - To clarify the function of a control that acts in an exceptional way (a button
2551  *   that opens a menu instead of performing an action directly, for example).
2553  * For a list of indicators included in the library, please see the
2554  * [OOjs UI documentation on MediaWiki] [1].
2556  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2558  * @abstract
2559  * @class
2561  * @constructor
2562  * @param {Object} [config] Configuration options
2563  * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2564  *  configuration is omitted, the indicator element will use a generated `<span>`.
2565  * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
2566  *  See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2567  *  in the library.
2568  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2569  * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2570  *  or a function that returns title text. The indicator title is displayed when users move
2571  *  the mouse over the indicator.
2572  */
2573 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
2574         // Configuration initialization
2575         config = config || {};
2577         // Properties
2578         this.$indicator = null;
2579         this.indicator = null;
2580         this.indicatorTitle = null;
2582         // Initialization
2583         this.setIndicator( config.indicator || this.constructor.static.indicator );
2584         this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
2585         this.setIndicatorElement( config.$indicator || $( '<span>' ) );
2588 /* Setup */
2590 OO.initClass( OO.ui.mixin.IndicatorElement );
2592 /* Static Properties */
2595  * Symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
2596  * The static property will be overridden if the #indicator configuration is used.
2598  * @static
2599  * @inheritable
2600  * @property {string|null}
2601  */
2602 OO.ui.mixin.IndicatorElement.static.indicator = null;
2605  * A text string used as the indicator title, a function that returns title text, or `null`
2606  * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2608  * @static
2609  * @inheritable
2610  * @property {string|Function|null}
2611  */
2612 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
2614 /* Methods */
2617  * Set the indicator element.
2619  * If an element is already set, it will be cleaned up before setting up the new element.
2621  * @param {jQuery} $indicator Element to use as indicator
2622  */
2623 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
2624         if ( this.$indicator ) {
2625                 this.$indicator
2626                         .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
2627                         .removeAttr( 'title' );
2628         }
2630         this.$indicator = $indicator
2631                 .addClass( 'oo-ui-indicatorElement-indicator' )
2632                 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
2633         if ( this.indicatorTitle !== null ) {
2634                 this.$indicator.attr( 'title', this.indicatorTitle );
2635         }
2637         this.updateThemeClasses();
2641  * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2643  * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2644  * @chainable
2645  */
2646 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
2647         indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
2649         if ( this.indicator !== indicator ) {
2650                 if ( this.$indicator ) {
2651                         if ( this.indicator !== null ) {
2652                                 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
2653                         }
2654                         if ( indicator !== null ) {
2655                                 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
2656                         }
2657                 }
2658                 this.indicator = indicator;
2659         }
2661         this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
2662         this.updateThemeClasses();
2664         return this;
2668  * Set the indicator title.
2670  * The title is displayed when a user moves the mouse over the indicator.
2672  * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2673  *   `null` for no indicator title
2674  * @chainable
2675  */
2676 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
2677         indicatorTitle = typeof indicatorTitle === 'function' ||
2678                 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
2679                         OO.ui.resolveMsg( indicatorTitle ) : null;
2681         if ( this.indicatorTitle !== indicatorTitle ) {
2682                 this.indicatorTitle = indicatorTitle;
2683                 if ( this.$indicator ) {
2684                         if ( this.indicatorTitle !== null ) {
2685                                 this.$indicator.attr( 'title', indicatorTitle );
2686                         } else {
2687                                 this.$indicator.removeAttr( 'title' );
2688                         }
2689                 }
2690         }
2692         return this;
2696  * Get the symbolic name of the indicator (e.g., ‘alert’ or  ‘down’).
2698  * @return {string} Symbolic name of indicator
2699  */
2700 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
2701         return this.indicator;
2705  * Get the indicator title.
2707  * The title is displayed when a user moves the mouse over the indicator.
2709  * @return {string} Indicator title text
2710  */
2711 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
2712         return this.indicatorTitle;
2716  * LabelElement is often mixed into other classes to generate a label, which
2717  * helps identify the function of an interface element.
2718  * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2720  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2722  * @abstract
2723  * @class
2725  * @constructor
2726  * @param {Object} [config] Configuration options
2727  * @cfg {jQuery} [$label] The label element created by the class. If this
2728  *  configuration is omitted, the label element will use a generated `<span>`.
2729  * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2730  *  as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2731  *  in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2732  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2733  */
2734 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
2735         // Configuration initialization
2736         config = config || {};
2738         // Properties
2739         this.$label = null;
2740         this.label = null;
2742         // Initialization
2743         this.setLabel( config.label || this.constructor.static.label );
2744         this.setLabelElement( config.$label || $( '<span>' ) );
2747 /* Setup */
2749 OO.initClass( OO.ui.mixin.LabelElement );
2751 /* Events */
2754  * @event labelChange
2755  * @param {string} value
2756  */
2758 /* Static Properties */
2761  * The label text. The label can be specified as a plaintext string, a function that will
2762  * produce a string in the future, or `null` for no label. The static value will
2763  * be overridden if a label is specified with the #label config option.
2765  * @static
2766  * @inheritable
2767  * @property {string|Function|null}
2768  */
2769 OO.ui.mixin.LabelElement.static.label = null;
2771 /* Static methods */
2774  * Highlight the first occurrence of the query in the given text
2776  * @param {string} text Text
2777  * @param {string} query Query to find
2778  * @return {jQuery} Text with the first match of the query
2779  *  sub-string wrapped in highlighted span
2780  */
2781 OO.ui.mixin.LabelElement.static.highlightQuery = function ( text, query ) {
2782         var $result = $( '<span>' ),
2783                 offset = text.toLowerCase().indexOf( query.toLowerCase() );
2785         if ( !query.length || offset === -1 ) {
2786                 return $result.text( text );
2787         }
2788         $result.append(
2789                 document.createTextNode( text.slice( 0, offset ) ),
2790                 $( '<span>' )
2791                         .addClass( 'oo-ui-labelElement-label-highlight' )
2792                         .text( text.slice( offset, offset + query.length ) ),
2793                 document.createTextNode( text.slice( offset + query.length ) )
2794         );
2795         return $result.contents();
2798 /* Methods */
2801  * Set the label element.
2803  * If an element is already set, it will be cleaned up before setting up the new element.
2805  * @param {jQuery} $label Element to use as label
2806  */
2807 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
2808         if ( this.$label ) {
2809                 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
2810         }
2812         this.$label = $label.addClass( 'oo-ui-labelElement-label' );
2813         this.setLabelContent( this.label );
2817  * Set the label.
2819  * An empty string will result in the label being hidden. A string containing only whitespace will
2820  * be converted to a single `&nbsp;`.
2822  * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2823  *  text; or null for no label
2824  * @chainable
2825  */
2826 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
2827         label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
2828         label = ( ( typeof label === 'string' || label instanceof jQuery ) && label.length ) || ( label instanceof OO.ui.HtmlSnippet && label.toString().length ) ? label : null;
2830         if ( this.label !== label ) {
2831                 if ( this.$label ) {
2832                         this.setLabelContent( label );
2833                 }
2834                 this.label = label;
2835                 this.emit( 'labelChange' );
2836         }
2838         this.$element.toggleClass( 'oo-ui-labelElement', !!this.label );
2840         return this;
2844  * Set the label as plain text with a highlighted query
2846  * @param {string} text Text label to set
2847  * @param {string} query Substring of text to highlight
2848  * @chainable
2849  */
2850 OO.ui.mixin.LabelElement.prototype.setHighlightedQuery = function ( text, query ) {
2851         return this.setLabel( this.constructor.static.highlightQuery( text, query ) );
2855  * Get the label.
2857  * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2858  *  text; or null for no label
2859  */
2860 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
2861         return this.label;
2865  * Fit the label.
2867  * @chainable
2868  * @deprecated since 0.16.0
2869  */
2870 OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
2871         return this;
2875  * Set the content of the label.
2877  * Do not call this method until after the label element has been set by #setLabelElement.
2879  * @private
2880  * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2881  *  text; or null for no label
2882  */
2883 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
2884         if ( typeof label === 'string' ) {
2885                 if ( label.match( /^\s*$/ ) ) {
2886                         // Convert whitespace only string to a single non-breaking space
2887                         this.$label.html( '&nbsp;' );
2888                 } else {
2889                         this.$label.text( label );
2890                 }
2891         } else if ( label instanceof OO.ui.HtmlSnippet ) {
2892                 this.$label.html( label.toString() );
2893         } else if ( label instanceof jQuery ) {
2894                 this.$label.empty().append( label );
2895         } else {
2896                 this.$label.empty();
2897         }
2901  * The FlaggedElement class is an attribute mixin, meaning that it is used to add
2902  * additional functionality to an element created by another class. The class provides
2903  * a ‘flags’ property assigned the name (or an array of names) of styling flags,
2904  * which are used to customize the look and feel of a widget to better describe its
2905  * importance and functionality.
2907  * The library currently contains the following styling flags for general use:
2909  * - **progressive**:  Progressive styling is applied to convey that the widget will move the user forward in a process.
2910  * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
2911  * - **constructive**: Constructive styling is applied to convey that the widget will create something.
2913  * The flags affect the appearance of the buttons:
2915  *     @example
2916  *     // FlaggedElement is mixed into ButtonWidget to provide styling flags
2917  *     var button1 = new OO.ui.ButtonWidget( {
2918  *         label: 'Constructive',
2919  *         flags: 'constructive'
2920  *     } );
2921  *     var button2 = new OO.ui.ButtonWidget( {
2922  *         label: 'Destructive',
2923  *         flags: 'destructive'
2924  *     } );
2925  *     var button3 = new OO.ui.ButtonWidget( {
2926  *         label: 'Progressive',
2927  *         flags: 'progressive'
2928  *     } );
2929  *     $( 'body' ).append( button1.$element, button2.$element, button3.$element );
2931  * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
2932  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
2934  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2936  * @abstract
2937  * @class
2939  * @constructor
2940  * @param {Object} [config] Configuration options
2941  * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
2942  *  Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
2943  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2944  * @cfg {jQuery} [$flagged] The flagged element. By default,
2945  *  the flagged functionality is applied to the element created by the class ($element).
2946  *  If a different element is specified, the flagged functionality will be applied to it instead.
2947  */
2948 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
2949         // Configuration initialization
2950         config = config || {};
2952         // Properties
2953         this.flags = {};
2954         this.$flagged = null;
2956         // Initialization
2957         this.setFlags( config.flags );
2958         this.setFlaggedElement( config.$flagged || this.$element );
2961 /* Events */
2964  * @event flag
2965  * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
2966  * parameter contains the name of each modified flag and indicates whether it was
2967  * added or removed.
2969  * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
2970  * that the flag was added, `false` that the flag was removed.
2971  */
2973 /* Methods */
2976  * Set the flagged element.
2978  * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
2979  * If an element is already set, the method will remove the mixin’s effect on that element.
2981  * @param {jQuery} $flagged Element that should be flagged
2982  */
2983 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
2984         var classNames = Object.keys( this.flags ).map( function ( flag ) {
2985                 return 'oo-ui-flaggedElement-' + flag;
2986         } ).join( ' ' );
2988         if ( this.$flagged ) {
2989                 this.$flagged.removeClass( classNames );
2990         }
2992         this.$flagged = $flagged.addClass( classNames );
2996  * Check if the specified flag is set.
2998  * @param {string} flag Name of flag
2999  * @return {boolean} The flag is set
3000  */
3001 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
3002         // This may be called before the constructor, thus before this.flags is set
3003         return this.flags && ( flag in this.flags );
3007  * Get the names of all flags set.
3009  * @return {string[]} Flag names
3010  */
3011 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
3012         // This may be called before the constructor, thus before this.flags is set
3013         return Object.keys( this.flags || {} );
3017  * Clear all flags.
3019  * @chainable
3020  * @fires flag
3021  */
3022 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
3023         var flag, className,
3024                 changes = {},
3025                 remove = [],
3026                 classPrefix = 'oo-ui-flaggedElement-';
3028         for ( flag in this.flags ) {
3029                 className = classPrefix + flag;
3030                 changes[ flag ] = false;
3031                 delete this.flags[ flag ];
3032                 remove.push( className );
3033         }
3035         if ( this.$flagged ) {
3036                 this.$flagged.removeClass( remove.join( ' ' ) );
3037         }
3039         this.updateThemeClasses();
3040         this.emit( 'flag', changes );
3042         return this;
3046  * Add one or more flags.
3048  * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3049  *  or an object keyed by flag name with a boolean value that indicates whether the flag should
3050  *  be added (`true`) or removed (`false`).
3051  * @chainable
3052  * @fires flag
3053  */
3054 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
3055         var i, len, flag, className,
3056                 changes = {},
3057                 add = [],
3058                 remove = [],
3059                 classPrefix = 'oo-ui-flaggedElement-';
3061         if ( typeof flags === 'string' ) {
3062                 className = classPrefix + flags;
3063                 // Set
3064                 if ( !this.flags[ flags ] ) {
3065                         this.flags[ flags ] = true;
3066                         add.push( className );
3067                 }
3068         } else if ( Array.isArray( flags ) ) {
3069                 for ( i = 0, len = flags.length; i < len; i++ ) {
3070                         flag = flags[ i ];
3071                         className = classPrefix + flag;
3072                         // Set
3073                         if ( !this.flags[ flag ] ) {
3074                                 changes[ flag ] = true;
3075                                 this.flags[ flag ] = true;
3076                                 add.push( className );
3077                         }
3078                 }
3079         } else if ( OO.isPlainObject( flags ) ) {
3080                 for ( flag in flags ) {
3081                         className = classPrefix + flag;
3082                         if ( flags[ flag ] ) {
3083                                 // Set
3084                                 if ( !this.flags[ flag ] ) {
3085                                         changes[ flag ] = true;
3086                                         this.flags[ flag ] = true;
3087                                         add.push( className );
3088                                 }
3089                         } else {
3090                                 // Remove
3091                                 if ( this.flags[ flag ] ) {
3092                                         changes[ flag ] = false;
3093                                         delete this.flags[ flag ];
3094                                         remove.push( className );
3095                                 }
3096                         }
3097                 }
3098         }
3100         if ( this.$flagged ) {
3101                 this.$flagged
3102                         .addClass( add.join( ' ' ) )
3103                         .removeClass( remove.join( ' ' ) );
3104         }
3106         this.updateThemeClasses();
3107         this.emit( 'flag', changes );
3109         return this;
3113  * TitledElement is mixed into other classes to provide a `title` attribute.
3114  * Titles are rendered by the browser and are made visible when the user moves
3115  * the mouse over the element. Titles are not visible on touch devices.
3117  *     @example
3118  *     // TitledElement provides a 'title' attribute to the
3119  *     // ButtonWidget class
3120  *     var button = new OO.ui.ButtonWidget( {
3121  *         label: 'Button with Title',
3122  *         title: 'I am a button'
3123  *     } );
3124  *     $( 'body' ).append( button.$element );
3126  * @abstract
3127  * @class
3129  * @constructor
3130  * @param {Object} [config] Configuration options
3131  * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3132  *  If this config is omitted, the title functionality is applied to $element, the
3133  *  element created by the class.
3134  * @cfg {string|Function} [title] The title text or a function that returns text. If
3135  *  this config is omitted, the value of the {@link #static-title static title} property is used.
3136  */
3137 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
3138         // Configuration initialization
3139         config = config || {};
3141         // Properties
3142         this.$titled = null;
3143         this.title = null;
3145         // Initialization
3146         this.setTitle( config.title !== undefined ? config.title : this.constructor.static.title );
3147         this.setTitledElement( config.$titled || this.$element );
3150 /* Setup */
3152 OO.initClass( OO.ui.mixin.TitledElement );
3154 /* Static Properties */
3157  * The title text, a function that returns text, or `null` for no title. The value of the static property
3158  * is overridden if the #title config option is used.
3160  * @static
3161  * @inheritable
3162  * @property {string|Function|null}
3163  */
3164 OO.ui.mixin.TitledElement.static.title = null;
3166 /* Methods */
3169  * Set the titled element.
3171  * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3172  * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3174  * @param {jQuery} $titled Element that should use the 'titled' functionality
3175  */
3176 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
3177         if ( this.$titled ) {
3178                 this.$titled.removeAttr( 'title' );
3179         }
3181         this.$titled = $titled;
3182         if ( this.title ) {
3183                 this.$titled.attr( 'title', this.title );
3184         }
3188  * Set title.
3190  * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3191  * @chainable
3192  */
3193 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
3194         title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
3195         title = ( typeof title === 'string' && title.length ) ? title : null;
3197         if ( this.title !== title ) {
3198                 if ( this.$titled ) {
3199                         if ( title !== null ) {
3200                                 this.$titled.attr( 'title', title );
3201                         } else {
3202                                 this.$titled.removeAttr( 'title' );
3203                         }
3204                 }
3205                 this.title = title;
3206         }
3208         return this;
3212  * Get title.
3214  * @return {string} Title string
3215  */
3216 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
3217         return this.title;
3221  * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3222  * Accesskeys allow an user to go to a specific element by using
3223  * a shortcut combination of a browser specific keys + the key
3224  * set to the field.
3226  *     @example
3227  *     // AccessKeyedElement provides an 'accesskey' attribute to the
3228  *     // ButtonWidget class
3229  *     var button = new OO.ui.ButtonWidget( {
3230  *         label: 'Button with Accesskey',
3231  *         accessKey: 'k'
3232  *     } );
3233  *     $( 'body' ).append( button.$element );
3235  * @abstract
3236  * @class
3238  * @constructor
3239  * @param {Object} [config] Configuration options
3240  * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3241  *  If this config is omitted, the accesskey functionality is applied to $element, the
3242  *  element created by the class.
3243  * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3244  *  this config is omitted, no accesskey will be added.
3245  */
3246 OO.ui.mixin.AccessKeyedElement = function OoUiMixinAccessKeyedElement( config ) {
3247         // Configuration initialization
3248         config = config || {};
3250         // Properties
3251         this.$accessKeyed = null;
3252         this.accessKey = null;
3254         // Initialization
3255         this.setAccessKey( config.accessKey || null );
3256         this.setAccessKeyedElement( config.$accessKeyed || this.$element );
3259 /* Setup */
3261 OO.initClass( OO.ui.mixin.AccessKeyedElement );
3263 /* Static Properties */
3266  * The access key, a function that returns a key, or `null` for no accesskey.
3268  * @static
3269  * @inheritable
3270  * @property {string|Function|null}
3271  */
3272 OO.ui.mixin.AccessKeyedElement.static.accessKey = null;
3274 /* Methods */
3277  * Set the accesskeyed element.
3279  * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3280  * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3282  * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3283  */
3284 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKeyedElement = function ( $accessKeyed ) {
3285         if ( this.$accessKeyed ) {
3286                 this.$accessKeyed.removeAttr( 'accesskey' );
3287         }
3289         this.$accessKeyed = $accessKeyed;
3290         if ( this.accessKey ) {
3291                 this.$accessKeyed.attr( 'accesskey', this.accessKey );
3292         }
3296  * Set accesskey.
3298  * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3299  * @chainable
3300  */
3301 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKey = function ( accessKey ) {
3302         accessKey = typeof accessKey === 'string' ? OO.ui.resolveMsg( accessKey ) : null;
3304         if ( this.accessKey !== accessKey ) {
3305                 if ( this.$accessKeyed ) {
3306                         if ( accessKey !== null ) {
3307                                 this.$accessKeyed.attr( 'accesskey', accessKey );
3308                         } else {
3309                                 this.$accessKeyed.removeAttr( 'accesskey' );
3310                         }
3311                 }
3312                 this.accessKey = accessKey;
3313         }
3315         return this;
3319  * Get accesskey.
3321  * @return {string} accessKey string
3322  */
3323 OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
3324         return this.accessKey;
3328  * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3329  * feels, and functionality can be customized via the class’s configuration options
3330  * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3331  * and examples.
3333  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3335  *     @example
3336  *     // A button widget
3337  *     var button = new OO.ui.ButtonWidget( {
3338  *         label: 'Button with Icon',
3339  *         icon: 'remove',
3340  *         iconTitle: 'Remove'
3341  *     } );
3342  *     $( 'body' ).append( button.$element );
3344  * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3346  * @class
3347  * @extends OO.ui.Widget
3348  * @mixins OO.ui.mixin.ButtonElement
3349  * @mixins OO.ui.mixin.IconElement
3350  * @mixins OO.ui.mixin.IndicatorElement
3351  * @mixins OO.ui.mixin.LabelElement
3352  * @mixins OO.ui.mixin.TitledElement
3353  * @mixins OO.ui.mixin.FlaggedElement
3354  * @mixins OO.ui.mixin.TabIndexedElement
3355  * @mixins OO.ui.mixin.AccessKeyedElement
3357  * @constructor
3358  * @param {Object} [config] Configuration options
3359  * @cfg {boolean} [active=false] Whether button should be shown as active
3360  * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3361  * @cfg {string} [target] The frame or window in which to open the hyperlink.
3362  * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3363  */
3364 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
3365         // Configuration initialization
3366         config = config || {};
3368         // Parent constructor
3369         OO.ui.ButtonWidget.parent.call( this, config );
3371         // Mixin constructors
3372         OO.ui.mixin.ButtonElement.call( this, config );
3373         OO.ui.mixin.IconElement.call( this, config );
3374         OO.ui.mixin.IndicatorElement.call( this, config );
3375         OO.ui.mixin.LabelElement.call( this, config );
3376         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
3377         OO.ui.mixin.FlaggedElement.call( this, config );
3378         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
3379         OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$button } ) );
3381         // Properties
3382         this.href = null;
3383         this.target = null;
3384         this.noFollow = false;
3386         // Events
3387         this.connect( this, { disable: 'onDisable' } );
3389         // Initialization
3390         this.$button.append( this.$icon, this.$label, this.$indicator );
3391         this.$element
3392                 .addClass( 'oo-ui-buttonWidget' )
3393                 .append( this.$button );
3394         this.setActive( config.active );
3395         this.setHref( config.href );
3396         this.setTarget( config.target );
3397         this.setNoFollow( config.noFollow );
3400 /* Setup */
3402 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
3403 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
3404 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
3405 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
3406 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
3407 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
3408 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
3409 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
3410 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
3412 /* Static Properties */
3415  * @inheritdoc
3416  */
3417 OO.ui.ButtonWidget.static.cancelButtonMouseDownEvents = false;
3419 /* Methods */
3422  * Get hyperlink location.
3424  * @return {string} Hyperlink location
3425  */
3426 OO.ui.ButtonWidget.prototype.getHref = function () {
3427         return this.href;
3431  * Get hyperlink target.
3433  * @return {string} Hyperlink target
3434  */
3435 OO.ui.ButtonWidget.prototype.getTarget = function () {
3436         return this.target;
3440  * Get search engine traversal hint.
3442  * @return {boolean} Whether search engines should avoid traversing this hyperlink
3443  */
3444 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
3445         return this.noFollow;
3449  * Set hyperlink location.
3451  * @param {string|null} href Hyperlink location, null to remove
3452  */
3453 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
3454         href = typeof href === 'string' ? href : null;
3455         if ( href !== null && !OO.ui.isSafeUrl( href ) ) {
3456                 href = './' + href;
3457         }
3459         if ( href !== this.href ) {
3460                 this.href = href;
3461                 this.updateHref();
3462         }
3464         return this;
3468  * Update the `href` attribute, in case of changes to href or
3469  * disabled state.
3471  * @private
3472  * @chainable
3473  */
3474 OO.ui.ButtonWidget.prototype.updateHref = function () {
3475         if ( this.href !== null && !this.isDisabled() ) {
3476                 this.$button.attr( 'href', this.href );
3477         } else {
3478                 this.$button.removeAttr( 'href' );
3479         }
3481         return this;
3485  * Handle disable events.
3487  * @private
3488  * @param {boolean} disabled Element is disabled
3489  */
3490 OO.ui.ButtonWidget.prototype.onDisable = function () {
3491         this.updateHref();
3495  * Set hyperlink target.
3497  * @param {string|null} target Hyperlink target, null to remove
3498  */
3499 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
3500         target = typeof target === 'string' ? target : null;
3502         if ( target !== this.target ) {
3503                 this.target = target;
3504                 if ( target !== null ) {
3505                         this.$button.attr( 'target', target );
3506                 } else {
3507                         this.$button.removeAttr( 'target' );
3508                 }
3509         }
3511         return this;
3515  * Set search engine traversal hint.
3517  * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3518  */
3519 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
3520         noFollow = typeof noFollow === 'boolean' ? noFollow : true;
3522         if ( noFollow !== this.noFollow ) {
3523                 this.noFollow = noFollow;
3524                 if ( noFollow ) {
3525                         this.$button.attr( 'rel', 'nofollow' );
3526                 } else {
3527                         this.$button.removeAttr( 'rel' );
3528                 }
3529         }
3531         return this;
3534 // Override method visibility hints from ButtonElement
3536  * @method setActive
3537  */
3539  * @method isActive
3540  */
3543  * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3544  * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3545  * removed, and cleared from the group.
3547  *     @example
3548  *     // Example: A ButtonGroupWidget with two buttons
3549  *     var button1 = new OO.ui.PopupButtonWidget( {
3550  *         label: 'Select a category',
3551  *         icon: 'menu',
3552  *         popup: {
3553  *             $content: $( '<p>List of categories...</p>' ),
3554  *             padded: true,
3555  *             align: 'left'
3556  *         }
3557  *     } );
3558  *     var button2 = new OO.ui.ButtonWidget( {
3559  *         label: 'Add item'
3560  *     });
3561  *     var buttonGroup = new OO.ui.ButtonGroupWidget( {
3562  *         items: [button1, button2]
3563  *     } );
3564  *     $( 'body' ).append( buttonGroup.$element );
3566  * @class
3567  * @extends OO.ui.Widget
3568  * @mixins OO.ui.mixin.GroupElement
3570  * @constructor
3571  * @param {Object} [config] Configuration options
3572  * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3573  */
3574 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
3575         // Configuration initialization
3576         config = config || {};
3578         // Parent constructor
3579         OO.ui.ButtonGroupWidget.parent.call( this, config );
3581         // Mixin constructors
3582         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
3584         // Initialization
3585         this.$element.addClass( 'oo-ui-buttonGroupWidget' );
3586         if ( Array.isArray( config.items ) ) {
3587                 this.addItems( config.items );
3588         }
3591 /* Setup */
3593 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
3594 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
3597  * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3598  * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3599  * for a list of icons included in the library.
3601  *     @example
3602  *     // An icon widget with a label
3603  *     var myIcon = new OO.ui.IconWidget( {
3604  *         icon: 'help',
3605  *         iconTitle: 'Help'
3606  *      } );
3607  *      // Create a label.
3608  *      var iconLabel = new OO.ui.LabelWidget( {
3609  *          label: 'Help'
3610  *      } );
3611  *      $( 'body' ).append( myIcon.$element, iconLabel.$element );
3613  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3615  * @class
3616  * @extends OO.ui.Widget
3617  * @mixins OO.ui.mixin.IconElement
3618  * @mixins OO.ui.mixin.TitledElement
3619  * @mixins OO.ui.mixin.FlaggedElement
3621  * @constructor
3622  * @param {Object} [config] Configuration options
3623  */
3624 OO.ui.IconWidget = function OoUiIconWidget( config ) {
3625         // Configuration initialization
3626         config = config || {};
3628         // Parent constructor
3629         OO.ui.IconWidget.parent.call( this, config );
3631         // Mixin constructors
3632         OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
3633         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
3634         OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
3636         // Initialization
3637         this.$element.addClass( 'oo-ui-iconWidget' );
3640 /* Setup */
3642 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
3643 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
3644 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
3645 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
3647 /* Static Properties */
3649 OO.ui.IconWidget.static.tagName = 'span';
3652  * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3653  * attention to the status of an item or to clarify the function of a control. For a list of
3654  * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3656  *     @example
3657  *     // Example of an indicator widget
3658  *     var indicator1 = new OO.ui.IndicatorWidget( {
3659  *         indicator: 'alert'
3660  *     } );
3662  *     // Create a fieldset layout to add a label
3663  *     var fieldset = new OO.ui.FieldsetLayout();
3664  *     fieldset.addItems( [
3665  *         new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3666  *     ] );
3667  *     $( 'body' ).append( fieldset.$element );
3669  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3671  * @class
3672  * @extends OO.ui.Widget
3673  * @mixins OO.ui.mixin.IndicatorElement
3674  * @mixins OO.ui.mixin.TitledElement
3676  * @constructor
3677  * @param {Object} [config] Configuration options
3678  */
3679 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
3680         // Configuration initialization
3681         config = config || {};
3683         // Parent constructor
3684         OO.ui.IndicatorWidget.parent.call( this, config );
3686         // Mixin constructors
3687         OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
3688         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
3690         // Initialization
3691         this.$element.addClass( 'oo-ui-indicatorWidget' );
3694 /* Setup */
3696 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
3697 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
3698 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
3700 /* Static Properties */
3702 OO.ui.IndicatorWidget.static.tagName = 'span';
3705  * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3706  * be configured with a `label` option that is set to a string, a label node, or a function:
3708  * - String: a plaintext string
3709  * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3710  *   label that includes a link or special styling, such as a gray color or additional graphical elements.
3711  * - Function: a function that will produce a string in the future. Functions are used
3712  *   in cases where the value of the label is not currently defined.
3714  * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3715  * will come into focus when the label is clicked.
3717  *     @example
3718  *     // Examples of LabelWidgets
3719  *     var label1 = new OO.ui.LabelWidget( {
3720  *         label: 'plaintext label'
3721  *     } );
3722  *     var label2 = new OO.ui.LabelWidget( {
3723  *         label: $( '<a href="default.html">jQuery label</a>' )
3724  *     } );
3725  *     // Create a fieldset layout with fields for each example
3726  *     var fieldset = new OO.ui.FieldsetLayout();
3727  *     fieldset.addItems( [
3728  *         new OO.ui.FieldLayout( label1 ),
3729  *         new OO.ui.FieldLayout( label2 )
3730  *     ] );
3731  *     $( 'body' ).append( fieldset.$element );
3733  * @class
3734  * @extends OO.ui.Widget
3735  * @mixins OO.ui.mixin.LabelElement
3736  * @mixins OO.ui.mixin.TitledElement
3738  * @constructor
3739  * @param {Object} [config] Configuration options
3740  * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
3741  *  Clicking the label will focus the specified input field.
3742  */
3743 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
3744         // Configuration initialization
3745         config = config || {};
3747         // Parent constructor
3748         OO.ui.LabelWidget.parent.call( this, config );
3750         // Mixin constructors
3751         OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
3752         OO.ui.mixin.TitledElement.call( this, config );
3754         // Properties
3755         this.input = config.input;
3757         // Events
3758         if ( this.input instanceof OO.ui.InputWidget ) {
3759                 this.$element.on( 'click', this.onClick.bind( this ) );
3760         }
3762         // Initialization
3763         this.$element.addClass( 'oo-ui-labelWidget' );
3766 /* Setup */
3768 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
3769 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
3770 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
3772 /* Static Properties */
3774 OO.ui.LabelWidget.static.tagName = 'span';
3776 /* Methods */
3779  * Handles label mouse click events.
3781  * @private
3782  * @param {jQuery.Event} e Mouse click event
3783  */
3784 OO.ui.LabelWidget.prototype.onClick = function () {
3785         this.input.simulateLabelClick();
3786         return false;
3790  * PendingElement is a mixin that is used to create elements that notify users that something is happening
3791  * and that they should wait before proceeding. The pending state is visually represented with a pending
3792  * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
3793  * field of a {@link OO.ui.TextInputWidget text input widget}.
3795  * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
3796  * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
3797  * in process dialogs.
3799  *     @example
3800  *     function MessageDialog( config ) {
3801  *         MessageDialog.parent.call( this, config );
3802  *     }
3803  *     OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
3805  *     MessageDialog.static.actions = [
3806  *         { action: 'save', label: 'Done', flags: 'primary' },
3807  *         { label: 'Cancel', flags: 'safe' }
3808  *     ];
3810  *     MessageDialog.prototype.initialize = function () {
3811  *         MessageDialog.parent.prototype.initialize.apply( this, arguments );
3812  *         this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
3813  *         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>' );
3814  *         this.$body.append( this.content.$element );
3815  *     };
3816  *     MessageDialog.prototype.getBodyHeight = function () {
3817  *         return 100;
3818  *     }
3819  *     MessageDialog.prototype.getActionProcess = function ( action ) {
3820  *         var dialog = this;
3821  *         if ( action === 'save' ) {
3822  *             dialog.getActions().get({actions: 'save'})[0].pushPending();
3823  *             return new OO.ui.Process()
3824  *             .next( 1000 )
3825  *             .next( function () {
3826  *                 dialog.getActions().get({actions: 'save'})[0].popPending();
3827  *             } );
3828  *         }
3829  *         return MessageDialog.parent.prototype.getActionProcess.call( this, action );
3830  *     };
3832  *     var windowManager = new OO.ui.WindowManager();
3833  *     $( 'body' ).append( windowManager.$element );
3835  *     var dialog = new MessageDialog();
3836  *     windowManager.addWindows( [ dialog ] );
3837  *     windowManager.openWindow( dialog );
3839  * @abstract
3840  * @class
3842  * @constructor
3843  * @param {Object} [config] Configuration options
3844  * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
3845  */
3846 OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
3847         // Configuration initialization
3848         config = config || {};
3850         // Properties
3851         this.pending = 0;
3852         this.$pending = null;
3854         // Initialisation
3855         this.setPendingElement( config.$pending || this.$element );
3858 /* Setup */
3860 OO.initClass( OO.ui.mixin.PendingElement );
3862 /* Methods */
3865  * Set the pending element (and clean up any existing one).
3867  * @param {jQuery} $pending The element to set to pending.
3868  */
3869 OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
3870         if ( this.$pending ) {
3871                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
3872         }
3874         this.$pending = $pending;
3875         if ( this.pending > 0 ) {
3876                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
3877         }
3881  * Check if an element is pending.
3883  * @return {boolean} Element is pending
3884  */
3885 OO.ui.mixin.PendingElement.prototype.isPending = function () {
3886         return !!this.pending;
3890  * Increase the pending counter. The pending state will remain active until the counter is zero
3891  * (i.e., the number of calls to #pushPending and #popPending is the same).
3893  * @chainable
3894  */
3895 OO.ui.mixin.PendingElement.prototype.pushPending = function () {
3896         if ( this.pending === 0 ) {
3897                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
3898                 this.updateThemeClasses();
3899         }
3900         this.pending++;
3902         return this;
3906  * Decrease the pending counter. The pending state will remain active until the counter is zero
3907  * (i.e., the number of calls to #pushPending and #popPending is the same).
3909  * @chainable
3910  */
3911 OO.ui.mixin.PendingElement.prototype.popPending = function () {
3912         if ( this.pending === 1 ) {
3913                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
3914                 this.updateThemeClasses();
3915         }
3916         this.pending = Math.max( 0, this.pending - 1 );
3918         return this;
3922  * Element that can be automatically clipped to visible boundaries.
3924  * Whenever the element's natural height changes, you have to call
3925  * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
3926  * clipping correctly.
3928  * The dimensions of #$clippableContainer will be compared to the boundaries of the
3929  * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
3930  * then #$clippable will be given a fixed reduced height and/or width and will be made
3931  * scrollable. By default, #$clippable and #$clippableContainer are the same element,
3932  * but you can build a static footer by setting #$clippableContainer to an element that contains
3933  * #$clippable and the footer.
3935  * @abstract
3936  * @class
3938  * @constructor
3939  * @param {Object} [config] Configuration options
3940  * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
3941  * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
3942  *   omit to use #$clippable
3943  */
3944 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
3945         // Configuration initialization
3946         config = config || {};
3948         // Properties
3949         this.$clippable = null;
3950         this.$clippableContainer = null;
3951         this.clipping = false;
3952         this.clippedHorizontally = false;
3953         this.clippedVertically = false;
3954         this.$clippableScrollableContainer = null;
3955         this.$clippableScroller = null;
3956         this.$clippableWindow = null;
3957         this.idealWidth = null;
3958         this.idealHeight = null;
3959         this.onClippableScrollHandler = this.clip.bind( this );
3960         this.onClippableWindowResizeHandler = this.clip.bind( this );
3962         // Initialization
3963         if ( config.$clippableContainer ) {
3964                 this.setClippableContainer( config.$clippableContainer );
3965         }
3966         this.setClippableElement( config.$clippable || this.$element );
3969 /* Methods */
3972  * Set clippable element.
3974  * If an element is already set, it will be cleaned up before setting up the new element.
3976  * @param {jQuery} $clippable Element to make clippable
3977  */
3978 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
3979         if ( this.$clippable ) {
3980                 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
3981                 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
3982                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
3983         }
3985         this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
3986         this.clip();
3990  * Set clippable container.
3992  * This is the container that will be measured when deciding whether to clip. When clipping,
3993  * #$clippable will be resized in order to keep the clippable container fully visible.
3995  * If the clippable container is unset, #$clippable will be used.
3997  * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
3998  */
3999 OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
4000         this.$clippableContainer = $clippableContainer;
4001         if ( this.$clippable ) {
4002                 this.clip();
4003         }
4007  * Toggle clipping.
4009  * Do not turn clipping on until after the element is attached to the DOM and visible.
4011  * @param {boolean} [clipping] Enable clipping, omit to toggle
4012  * @chainable
4013  */
4014 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
4015         clipping = clipping === undefined ? !this.clipping : !!clipping;
4017         if ( this.clipping !== clipping ) {
4018                 this.clipping = clipping;
4019                 if ( clipping ) {
4020                         this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
4021                         // If the clippable container is the root, we have to listen to scroll events and check
4022                         // jQuery.scrollTop on the window because of browser inconsistencies
4023                         this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
4024                                 $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
4025                                 this.$clippableScrollableContainer;
4026                         this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
4027                         this.$clippableWindow = $( this.getElementWindow() )
4028                                 .on( 'resize', this.onClippableWindowResizeHandler );
4029                         // Initial clip after visible
4030                         this.clip();
4031                 } else {
4032                         this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
4033                         OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
4035                         this.$clippableScrollableContainer = null;
4036                         this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
4037                         this.$clippableScroller = null;
4038                         this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
4039                         this.$clippableWindow = null;
4040                 }
4041         }
4043         return this;
4047  * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4049  * @return {boolean} Element will be clipped to the visible area
4050  */
4051 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
4052         return this.clipping;
4056  * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4058  * @return {boolean} Part of the element is being clipped
4059  */
4060 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
4061         return this.clippedHorizontally || this.clippedVertically;
4065  * Check if the right of the element is being clipped by the nearest scrollable container.
4067  * @return {boolean} Part of the element is being clipped
4068  */
4069 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
4070         return this.clippedHorizontally;
4074  * Check if the bottom of the element is being clipped by the nearest scrollable container.
4076  * @return {boolean} Part of the element is being clipped
4077  */
4078 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
4079         return this.clippedVertically;
4083  * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
4085  * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4086  * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4087  */
4088 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
4089         this.idealWidth = width;
4090         this.idealHeight = height;
4092         if ( !this.clipping ) {
4093                 // Update dimensions
4094                 this.$clippable.css( { width: width, height: height } );
4095         }
4096         // While clipping, idealWidth and idealHeight are not considered
4100  * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4101  * when the element's natural height changes.
4103  * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4104  * overlapped by, the visible area of the nearest scrollable container.
4106  * Because calling clip() when the natural height changes isn't always possible, we also set
4107  * max-height when the element isn't being clipped. This means that if the element tries to grow
4108  * beyond the edge, something reasonable will happen before clip() is called.
4110  * @chainable
4111  */
4112 OO.ui.mixin.ClippableElement.prototype.clip = function () {
4113         var $container, extraHeight, extraWidth, ccOffset,
4114                 $scrollableContainer, scOffset, scHeight, scWidth,
4115                 ccWidth, scrollerIsWindow, scrollTop, scrollLeft,
4116                 desiredWidth, desiredHeight, allotedWidth, allotedHeight,
4117                 naturalWidth, naturalHeight, clipWidth, clipHeight,
4118                 buffer = 7; // Chosen by fair dice roll
4120         if ( !this.clipping ) {
4121                 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4122                 return this;
4123         }
4125         $container = this.$clippableContainer || this.$clippable;
4126         extraHeight = $container.outerHeight() - this.$clippable.outerHeight();
4127         extraWidth = $container.outerWidth() - this.$clippable.outerWidth();
4128         ccOffset = $container.offset();
4129         if ( this.$clippableScrollableContainer.is( 'html, body' ) ) {
4130                 $scrollableContainer = this.$clippableWindow;
4131                 scOffset = { top: 0, left: 0 };
4132         } else {
4133                 $scrollableContainer = this.$clippableScrollableContainer;
4134                 scOffset = $scrollableContainer.offset();
4135         }
4136         scHeight = $scrollableContainer.innerHeight() - buffer;
4137         scWidth = $scrollableContainer.innerWidth() - buffer;
4138         ccWidth = $container.outerWidth() + buffer;
4139         scrollerIsWindow = this.$clippableScroller[ 0 ] === this.$clippableWindow[ 0 ];
4140         scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0;
4141         scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0;
4142         desiredWidth = ccOffset.left < 0 ?
4143                 ccWidth + ccOffset.left :
4144                 ( scOffset.left + scrollLeft + scWidth ) - ccOffset.left;
4145         desiredHeight = ( scOffset.top + scrollTop + scHeight ) - ccOffset.top;
4146         // It should never be desirable to exceed the dimensions of the browser viewport... right?
4147         desiredWidth = Math.min( desiredWidth, document.documentElement.clientWidth );
4148         desiredHeight = Math.min( desiredHeight, document.documentElement.clientHeight );
4149         allotedWidth = Math.ceil( desiredWidth - extraWidth );
4150         allotedHeight = Math.ceil( desiredHeight - extraHeight );
4151         naturalWidth = this.$clippable.prop( 'scrollWidth' );
4152         naturalHeight = this.$clippable.prop( 'scrollHeight' );
4153         clipWidth = allotedWidth < naturalWidth;
4154         clipHeight = allotedHeight < naturalHeight;
4156         if ( clipWidth ) {
4157                 this.$clippable.css( {
4158                         overflowX: 'scroll',
4159                         width: Math.max( 0, allotedWidth ),
4160                         maxWidth: ''
4161                 } );
4162         } else {
4163                 this.$clippable.css( {
4164                         overflowX: '',
4165                         width: this.idealWidth ? this.idealWidth - extraWidth : '',
4166                         maxWidth: Math.max( 0, allotedWidth )
4167                 } );
4168         }
4169         if ( clipHeight ) {
4170                 this.$clippable.css( {
4171                         overflowY: 'scroll',
4172                         height: Math.max( 0, allotedHeight ),
4173                         maxHeight: ''
4174                 } );
4175         } else {
4176                 this.$clippable.css( {
4177                         overflowY: '',
4178                         height: this.idealHeight ? this.idealHeight - extraHeight : '',
4179                         maxHeight: Math.max( 0, allotedHeight )
4180                 } );
4181         }
4183         // If we stopped clipping in at least one of the dimensions
4184         if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
4185                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
4186         }
4188         this.clippedHorizontally = clipWidth;
4189         this.clippedVertically = clipHeight;
4191         return this;
4195  * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4196  * By default, each popup has an anchor that points toward its origin.
4197  * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4199  *     @example
4200  *     // A popup widget.
4201  *     var popup = new OO.ui.PopupWidget( {
4202  *         $content: $( '<p>Hi there!</p>' ),
4203  *         padded: true,
4204  *         width: 300
4205  *     } );
4207  *     $( 'body' ).append( popup.$element );
4208  *     // To display the popup, toggle the visibility to 'true'.
4209  *     popup.toggle( true );
4211  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4213  * @class
4214  * @extends OO.ui.Widget
4215  * @mixins OO.ui.mixin.LabelElement
4216  * @mixins OO.ui.mixin.ClippableElement
4218  * @constructor
4219  * @param {Object} [config] Configuration options
4220  * @cfg {number} [width=320] Width of popup in pixels
4221  * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4222  * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4223  * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
4224  *  If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
4225  *  popup is leaning towards the right of the screen.
4226  *  Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
4227  *  in the given language, which means it will flip to the correct positioning in right-to-left languages.
4228  *  Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
4229  *  sentence in the given language.
4230  * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4231  *  See the [OOjs UI docs on MediaWiki][3] for an example.
4232  *  [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4233  * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4234  * @cfg {jQuery} [$content] Content to append to the popup's body
4235  * @cfg {jQuery} [$footer] Content to append to the popup's footer
4236  * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4237  * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4238  *  This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4239  *  for an example.
4240  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4241  * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
4242  *  button.
4243  * @cfg {boolean} [padded=false] Add padding to the popup's body
4244  */
4245 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
4246         // Configuration initialization
4247         config = config || {};
4249         // Parent constructor
4250         OO.ui.PopupWidget.parent.call( this, config );
4252         // Properties (must be set before ClippableElement constructor call)
4253         this.$body = $( '<div>' );
4254         this.$popup = $( '<div>' );
4256         // Mixin constructors
4257         OO.ui.mixin.LabelElement.call( this, config );
4258         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, {
4259                 $clippable: this.$body,
4260                 $clippableContainer: this.$popup
4261         } ) );
4263         // Properties
4264         this.$anchor = $( '<div>' );
4265         // If undefined, will be computed lazily in updateDimensions()
4266         this.$container = config.$container;
4267         this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
4268         this.autoClose = !!config.autoClose;
4269         this.$autoCloseIgnore = config.$autoCloseIgnore;
4270         this.transitionTimeout = null;
4271         this.anchor = null;
4272         this.width = config.width !== undefined ? config.width : 320;
4273         this.height = config.height !== undefined ? config.height : null;
4274         this.setAlignment( config.align );
4275         this.onMouseDownHandler = this.onMouseDown.bind( this );
4276         this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
4278         // Initialization
4279         this.toggleAnchor( config.anchor === undefined || config.anchor );
4280         this.$body.addClass( 'oo-ui-popupWidget-body' );
4281         this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
4282         this.$popup
4283                 .addClass( 'oo-ui-popupWidget-popup' )
4284                 .append( this.$body );
4285         this.$element
4286                 .addClass( 'oo-ui-popupWidget' )
4287                 .append( this.$popup, this.$anchor );
4288         // Move content, which was added to #$element by OO.ui.Widget, to the body
4289         // FIXME This is gross, we should use '$body' or something for the config
4290         if ( config.$content instanceof jQuery ) {
4291                 this.$body.append( config.$content );
4292         }
4294         if ( config.padded ) {
4295                 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
4296         }
4298         if ( config.head ) {
4299                 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
4300                 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
4301                 this.$head = $( '<div>' )
4302                         .addClass( 'oo-ui-popupWidget-head' )
4303                         .append( this.$label, this.closeButton.$element );
4304                 this.$popup.prepend( this.$head );
4305         }
4307         if ( config.$footer ) {
4308                 this.$footer = $( '<div>' )
4309                         .addClass( 'oo-ui-popupWidget-footer' )
4310                         .append( config.$footer );
4311                 this.$popup.append( this.$footer );
4312         }
4314         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
4315         // that reference properties not initialized at that time of parent class construction
4316         // TODO: Find a better way to handle post-constructor setup
4317         this.visible = false;
4318         this.$element.addClass( 'oo-ui-element-hidden' );
4321 /* Setup */
4323 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
4324 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
4325 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
4327 /* Methods */
4330  * Handles mouse down events.
4332  * @private
4333  * @param {MouseEvent} e Mouse down event
4334  */
4335 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
4336         if (
4337                 this.isVisible() &&
4338                 !OO.ui.contains( this.$element.add( this.$autoCloseIgnore ).get(), e.target, true )
4339         ) {
4340                 this.toggle( false );
4341         }
4345  * Bind mouse down listener.
4347  * @private
4348  */
4349 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
4350         // Capture clicks outside popup
4351         this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
4355  * Handles close button click events.
4357  * @private
4358  */
4359 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
4360         if ( this.isVisible() ) {
4361                 this.toggle( false );
4362         }
4366  * Unbind mouse down listener.
4368  * @private
4369  */
4370 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
4371         this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
4375  * Handles key down events.
4377  * @private
4378  * @param {KeyboardEvent} e Key down event
4379  */
4380 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
4381         if (
4382                 e.which === OO.ui.Keys.ESCAPE &&
4383                 this.isVisible()
4384         ) {
4385                 this.toggle( false );
4386                 e.preventDefault();
4387                 e.stopPropagation();
4388         }
4392  * Bind key down listener.
4394  * @private
4395  */
4396 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
4397         this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
4401  * Unbind key down listener.
4403  * @private
4404  */
4405 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
4406         this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
4410  * Show, hide, or toggle the visibility of the anchor.
4412  * @param {boolean} [show] Show anchor, omit to toggle
4413  */
4414 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
4415         show = show === undefined ? !this.anchored : !!show;
4417         if ( this.anchored !== show ) {
4418                 if ( show ) {
4419                         this.$element.addClass( 'oo-ui-popupWidget-anchored' );
4420                 } else {
4421                         this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
4422                 }
4423                 this.anchored = show;
4424         }
4428  * Check if the anchor is visible.
4430  * @return {boolean} Anchor is visible
4431  */
4432 OO.ui.PopupWidget.prototype.hasAnchor = function () {
4433         return this.anchor;
4437  * @inheritdoc
4438  */
4439 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
4440         var change;
4441         show = show === undefined ? !this.isVisible() : !!show;
4443         change = show !== this.isVisible();
4445         // Parent method
4446         OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
4448         if ( change ) {
4449                 if ( show ) {
4450                         if ( this.autoClose ) {
4451                                 this.bindMouseDownListener();
4452                                 this.bindKeyDownListener();
4453                         }
4454                         this.updateDimensions();
4455                         this.toggleClipping( true );
4456                 } else {
4457                         this.toggleClipping( false );
4458                         if ( this.autoClose ) {
4459                                 this.unbindMouseDownListener();
4460                                 this.unbindKeyDownListener();
4461                         }
4462                 }
4463         }
4465         return this;
4469  * Set the size of the popup.
4471  * Changing the size may also change the popup's position depending on the alignment.
4473  * @param {number} width Width in pixels
4474  * @param {number} height Height in pixels
4475  * @param {boolean} [transition=false] Use a smooth transition
4476  * @chainable
4477  */
4478 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
4479         this.width = width;
4480         this.height = height !== undefined ? height : null;
4481         if ( this.isVisible() ) {
4482                 this.updateDimensions( transition );
4483         }
4487  * Update the size and position.
4489  * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
4490  * be called automatically.
4492  * @param {boolean} [transition=false] Use a smooth transition
4493  * @chainable
4494  */
4495 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
4496         var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
4497                 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
4498                 align = this.align,
4499                 widget = this;
4501         if ( !this.$container ) {
4502                 // Lazy-initialize $container if not specified in constructor
4503                 this.$container = $( this.getClosestScrollableElementContainer() );
4504         }
4506         // Set height and width before measuring things, since it might cause our measurements
4507         // to change (e.g. due to scrollbars appearing or disappearing)
4508         this.$popup.css( {
4509                 width: this.width,
4510                 height: this.height !== null ? this.height : 'auto'
4511         } );
4513         // If we are in RTL, we need to flip the alignment, unless it is center
4514         if ( align === 'forwards' || align === 'backwards' ) {
4515                 if ( this.$container.css( 'direction' ) === 'rtl' ) {
4516                         align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
4517                 } else {
4518                         align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
4519                 }
4521         }
4523         // Compute initial popupOffset based on alignment
4524         popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
4526         // Figure out if this will cause the popup to go beyond the edge of the container
4527         originOffset = this.$element.offset().left;
4528         containerLeft = this.$container.offset().left;
4529         containerWidth = this.$container.innerWidth();
4530         containerRight = containerLeft + containerWidth;
4531         popupLeft = popupOffset - this.containerPadding;
4532         popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
4533         overlapLeft = ( originOffset + popupLeft ) - containerLeft;
4534         overlapRight = containerRight - ( originOffset + popupRight );
4536         // Adjust offset to make the popup not go beyond the edge, if needed
4537         if ( overlapRight < 0 ) {
4538                 popupOffset += overlapRight;
4539         } else if ( overlapLeft < 0 ) {
4540                 popupOffset -= overlapLeft;
4541         }
4543         // Adjust offset to avoid anchor being rendered too close to the edge
4544         // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
4545         // TODO: Find a measurement that works for CSS anchors and image anchors
4546         anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
4547         if ( popupOffset + this.width < anchorWidth ) {
4548                 popupOffset = anchorWidth - this.width;
4549         } else if ( -popupOffset < anchorWidth ) {
4550                 popupOffset = -anchorWidth;
4551         }
4553         // Prevent transition from being interrupted
4554         clearTimeout( this.transitionTimeout );
4555         if ( transition ) {
4556                 // Enable transition
4557                 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
4558         }
4560         // Position body relative to anchor
4561         this.$popup.css( 'margin-left', popupOffset );
4563         if ( transition ) {
4564                 // Prevent transitioning after transition is complete
4565                 this.transitionTimeout = setTimeout( function () {
4566                         widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
4567                 }, 200 );
4568         } else {
4569                 // Prevent transitioning immediately
4570                 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
4571         }
4573         // Reevaluate clipping state since we've relocated and resized the popup
4574         this.clip();
4576         return this;
4580  * Set popup alignment
4582  * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4583  *  `backwards` or `forwards`.
4584  */
4585 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
4586         // Validate alignment and transform deprecated values
4587         if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
4588                 this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
4589         } else {
4590                 this.align = 'center';
4591         }
4595  * Get popup alignment
4597  * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4598  *  `backwards` or `forwards`.
4599  */
4600 OO.ui.PopupWidget.prototype.getAlignment = function () {
4601         return this.align;
4605  * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
4606  * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
4607  * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
4608  * See {@link OO.ui.PopupWidget PopupWidget} for an example.
4610  * @abstract
4611  * @class
4613  * @constructor
4614  * @param {Object} [config] Configuration options
4615  * @cfg {Object} [popup] Configuration to pass to popup
4616  * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
4617  */
4618 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
4619         // Configuration initialization
4620         config = config || {};
4622         // Properties
4623         this.popup = new OO.ui.PopupWidget( $.extend(
4624                 { autoClose: true },
4625                 config.popup,
4626                 { $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore ) }
4627         ) );
4630 /* Methods */
4633  * Get popup.
4635  * @return {OO.ui.PopupWidget} Popup widget
4636  */
4637 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
4638         return this.popup;
4642  * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
4643  * which is used to display additional information or options.
4645  *     @example
4646  *     // Example of a popup button.
4647  *     var popupButton = new OO.ui.PopupButtonWidget( {
4648  *         label: 'Popup button with options',
4649  *         icon: 'menu',
4650  *         popup: {
4651  *             $content: $( '<p>Additional options here.</p>' ),
4652  *             padded: true,
4653  *             align: 'force-left'
4654  *         }
4655  *     } );
4656  *     // Append the button to the DOM.
4657  *     $( 'body' ).append( popupButton.$element );
4659  * @class
4660  * @extends OO.ui.ButtonWidget
4661  * @mixins OO.ui.mixin.PopupElement
4663  * @constructor
4664  * @param {Object} [config] Configuration options
4665  */
4666 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
4667         // Parent constructor
4668         OO.ui.PopupButtonWidget.parent.call( this, config );
4670         // Mixin constructors
4671         OO.ui.mixin.PopupElement.call( this, config );
4673         // Events
4674         this.connect( this, { click: 'onAction' } );
4676         // Initialization
4677         this.$element
4678                 .addClass( 'oo-ui-popupButtonWidget' )
4679                 .attr( 'aria-haspopup', 'true' )
4680                 .append( this.popup.$element );
4683 /* Setup */
4685 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
4686 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
4688 /* Methods */
4691  * Handle the button action being triggered.
4693  * @private
4694  */
4695 OO.ui.PopupButtonWidget.prototype.onAction = function () {
4696         this.popup.toggle();
4700  * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
4702  * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
4704  * @private
4705  * @abstract
4706  * @class
4707  * @mixins OO.ui.mixin.GroupElement
4709  * @constructor
4710  * @param {Object} [config] Configuration options
4711  */
4712 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
4713         // Mixin constructors
4714         OO.ui.mixin.GroupElement.call( this, config );
4717 /* Setup */
4719 OO.mixinClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
4721 /* Methods */
4724  * Set the disabled state of the widget.
4726  * This will also update the disabled state of child widgets.
4728  * @param {boolean} disabled Disable widget
4729  * @chainable
4730  */
4731 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
4732         var i, len;
4734         // Parent method
4735         // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
4736         OO.ui.Widget.prototype.setDisabled.call( this, disabled );
4738         // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
4739         if ( this.items ) {
4740                 for ( i = 0, len = this.items.length; i < len; i++ ) {
4741                         this.items[ i ].updateDisabled();
4742                 }
4743         }
4745         return this;
4749  * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
4751  * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
4752  * allows bidirectional communication.
4754  * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
4756  * @private
4757  * @abstract
4758  * @class
4760  * @constructor
4761  */
4762 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
4763         //
4766 /* Methods */
4769  * Check if widget is disabled.
4771  * Checks parent if present, making disabled state inheritable.
4773  * @return {boolean} Widget is disabled
4774  */
4775 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
4776         return this.disabled ||
4777                 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
4781  * Set group element is in.
4783  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
4784  * @chainable
4785  */
4786 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
4787         // Parent method
4788         // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
4789         OO.ui.Element.prototype.setElementGroup.call( this, group );
4791         // Initialize item disabled states
4792         this.updateDisabled();
4794         return this;
4798  * OptionWidgets are special elements that can be selected and configured with data. The
4799  * data is often unique for each option, but it does not have to be. OptionWidgets are used
4800  * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
4801  * and examples, please see the [OOjs UI documentation on MediaWiki][1].
4803  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4805  * @class
4806  * @extends OO.ui.Widget
4807  * @mixins OO.ui.mixin.ItemWidget
4808  * @mixins OO.ui.mixin.LabelElement
4809  * @mixins OO.ui.mixin.FlaggedElement
4810  * @mixins OO.ui.mixin.AccessKeyedElement
4812  * @constructor
4813  * @param {Object} [config] Configuration options
4814  */
4815 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
4816         // Configuration initialization
4817         config = config || {};
4819         // Parent constructor
4820         OO.ui.OptionWidget.parent.call( this, config );
4822         // Mixin constructors
4823         OO.ui.mixin.ItemWidget.call( this );
4824         OO.ui.mixin.LabelElement.call( this, config );
4825         OO.ui.mixin.FlaggedElement.call( this, config );
4826         OO.ui.mixin.AccessKeyedElement.call( this, config );
4828         // Properties
4829         this.selected = false;
4830         this.highlighted = false;
4831         this.pressed = false;
4833         // Initialization
4834         this.$element
4835                 .data( 'oo-ui-optionWidget', this )
4836                 // Allow programmatic focussing (and by accesskey), but not tabbing
4837                 .attr( 'tabindex', '-1' )
4838                 .attr( 'role', 'option' )
4839                 .attr( 'aria-selected', 'false' )
4840                 .addClass( 'oo-ui-optionWidget' )
4841                 .append( this.$label );
4844 /* Setup */
4846 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
4847 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
4848 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
4849 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
4850 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.AccessKeyedElement );
4852 /* Static Properties */
4854 OO.ui.OptionWidget.static.selectable = true;
4856 OO.ui.OptionWidget.static.highlightable = true;
4858 OO.ui.OptionWidget.static.pressable = true;
4860 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
4862 /* Methods */
4865  * Check if the option can be selected.
4867  * @return {boolean} Item is selectable
4868  */
4869 OO.ui.OptionWidget.prototype.isSelectable = function () {
4870         return this.constructor.static.selectable && !this.isDisabled() && this.isVisible();
4874  * Check if the option can be highlighted. A highlight indicates that the option
4875  * may be selected when a user presses enter or clicks. Disabled items cannot
4876  * be highlighted.
4878  * @return {boolean} Item is highlightable
4879  */
4880 OO.ui.OptionWidget.prototype.isHighlightable = function () {
4881         return this.constructor.static.highlightable && !this.isDisabled() && this.isVisible();
4885  * Check if the option can be pressed. The pressed state occurs when a user mouses
4886  * down on an item, but has not yet let go of the mouse.
4888  * @return {boolean} Item is pressable
4889  */
4890 OO.ui.OptionWidget.prototype.isPressable = function () {
4891         return this.constructor.static.pressable && !this.isDisabled() && this.isVisible();
4895  * Check if the option is selected.
4897  * @return {boolean} Item is selected
4898  */
4899 OO.ui.OptionWidget.prototype.isSelected = function () {
4900         return this.selected;
4904  * Check if the option is highlighted. A highlight indicates that the
4905  * item may be selected when a user presses enter or clicks.
4907  * @return {boolean} Item is highlighted
4908  */
4909 OO.ui.OptionWidget.prototype.isHighlighted = function () {
4910         return this.highlighted;
4914  * Check if the option is pressed. The pressed state occurs when a user mouses
4915  * down on an item, but has not yet let go of the mouse. The item may appear
4916  * selected, but it will not be selected until the user releases the mouse.
4918  * @return {boolean} Item is pressed
4919  */
4920 OO.ui.OptionWidget.prototype.isPressed = function () {
4921         return this.pressed;
4925  * Set the option’s selected state. In general, all modifications to the selection
4926  * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
4927  * method instead of this method.
4929  * @param {boolean} [state=false] Select option
4930  * @chainable
4931  */
4932 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
4933         if ( this.constructor.static.selectable ) {
4934                 this.selected = !!state;
4935                 this.$element
4936                         .toggleClass( 'oo-ui-optionWidget-selected', state )
4937                         .attr( 'aria-selected', state.toString() );
4938                 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
4939                         this.scrollElementIntoView();
4940                 }
4941                 this.updateThemeClasses();
4942         }
4943         return this;
4947  * Set the option’s highlighted state. In general, all programmatic
4948  * modifications to the highlight should be handled by the
4949  * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
4950  * method instead of this method.
4952  * @param {boolean} [state=false] Highlight option
4953  * @chainable
4954  */
4955 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
4956         if ( this.constructor.static.highlightable ) {
4957                 this.highlighted = !!state;
4958                 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
4959                 this.updateThemeClasses();
4960         }
4961         return this;
4965  * Set the option’s pressed state. In general, all
4966  * programmatic modifications to the pressed state should be handled by the
4967  * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
4968  * method instead of this method.
4970  * @param {boolean} [state=false] Press option
4971  * @chainable
4972  */
4973 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
4974         if ( this.constructor.static.pressable ) {
4975                 this.pressed = !!state;
4976                 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
4977                 this.updateThemeClasses();
4978         }
4979         return this;
4983  * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
4984  * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
4985  * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
4986  * menu selects}.
4988  * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
4989  * information, please see the [OOjs UI documentation on MediaWiki][1].
4991  *     @example
4992  *     // Example of a select widget with three options
4993  *     var select = new OO.ui.SelectWidget( {
4994  *         items: [
4995  *             new OO.ui.OptionWidget( {
4996  *                 data: 'a',
4997  *                 label: 'Option One',
4998  *             } ),
4999  *             new OO.ui.OptionWidget( {
5000  *                 data: 'b',
5001  *                 label: 'Option Two',
5002  *             } ),
5003  *             new OO.ui.OptionWidget( {
5004  *                 data: 'c',
5005  *                 label: 'Option Three',
5006  *             } )
5007  *         ]
5008  *     } );
5009  *     $( 'body' ).append( select.$element );
5011  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5013  * @abstract
5014  * @class
5015  * @extends OO.ui.Widget
5016  * @mixins OO.ui.mixin.GroupWidget
5018  * @constructor
5019  * @param {Object} [config] Configuration options
5020  * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
5021  *  Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
5022  *  the [OOjs UI documentation on MediaWiki] [2] for examples.
5023  *  [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5024  */
5025 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
5026         // Configuration initialization
5027         config = config || {};
5029         // Parent constructor
5030         OO.ui.SelectWidget.parent.call( this, config );
5032         // Mixin constructors
5033         OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
5035         // Properties
5036         this.pressed = false;
5037         this.selecting = null;
5038         this.onMouseUpHandler = this.onMouseUp.bind( this );
5039         this.onMouseMoveHandler = this.onMouseMove.bind( this );
5040         this.onKeyDownHandler = this.onKeyDown.bind( this );
5041         this.onKeyPressHandler = this.onKeyPress.bind( this );
5042         this.keyPressBuffer = '';
5043         this.keyPressBufferTimer = null;
5044         this.blockMouseOverEvents = 0;
5046         // Events
5047         this.connect( this, {
5048                 toggle: 'onToggle'
5049         } );
5050         this.$element.on( {
5051                 focusin: this.onFocus.bind( this ),
5052                 mousedown: this.onMouseDown.bind( this ),
5053                 mouseover: this.onMouseOver.bind( this ),
5054                 mouseleave: this.onMouseLeave.bind( this )
5055         } );
5057         // Initialization
5058         this.$element
5059                 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
5060                 .attr( 'role', 'listbox' );
5061         if ( Array.isArray( config.items ) ) {
5062                 this.addItems( config.items );
5063         }
5066 /* Setup */
5068 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
5069 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
5071 /* Events */
5074  * @event highlight
5076  * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
5078  * @param {OO.ui.OptionWidget|null} item Highlighted item
5079  */
5082  * @event press
5084  * A `press` event is emitted when the #pressItem method is used to programmatically modify the
5085  * pressed state of an option.
5087  * @param {OO.ui.OptionWidget|null} item Pressed item
5088  */
5091  * @event select
5093  * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
5095  * @param {OO.ui.OptionWidget|null} item Selected item
5096  */
5099  * @event choose
5100  * A `choose` event is emitted when an item is chosen with the #chooseItem method.
5101  * @param {OO.ui.OptionWidget} item Chosen item
5102  */
5105  * @event add
5107  * An `add` event is emitted when options are added to the select with the #addItems method.
5109  * @param {OO.ui.OptionWidget[]} items Added items
5110  * @param {number} index Index of insertion point
5111  */
5114  * @event remove
5116  * A `remove` event is emitted when options are removed from the select with the #clearItems
5117  * or #removeItems methods.
5119  * @param {OO.ui.OptionWidget[]} items Removed items
5120  */
5122 /* Methods */
5125  * Handle focus events
5127  * @private
5128  * @param {jQuery.Event} event
5129  */
5130 OO.ui.SelectWidget.prototype.onFocus = function ( event ) {
5131         var item;
5132         if ( event.target === this.$element[ 0 ] ) {
5133                 // This widget was focussed, e.g. by the user tabbing to it.
5134                 // The styles for focus state depend on one of the items being selected.
5135                 if ( !this.getSelectedItem() ) {
5136                         item = this.getFirstSelectableItem();
5137                 }
5138         } else {
5139                 // One of the options got focussed (and the event bubbled up here).
5140                 // They can't be tabbed to, but they can be activated using accesskeys.
5141                 item = this.getTargetItem( event );
5142         }
5144         if ( item ) {
5145                 if ( item.constructor.static.highlightable ) {
5146                         this.highlightItem( item );
5147                 } else {
5148                         this.selectItem( item );
5149                 }
5150         }
5152         if ( event.target !== this.$element[ 0 ] ) {
5153                 this.$element.focus();
5154         }
5158  * Handle mouse down events.
5160  * @private
5161  * @param {jQuery.Event} e Mouse down event
5162  */
5163 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
5164         var item;
5166         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
5167                 this.togglePressed( true );
5168                 item = this.getTargetItem( e );
5169                 if ( item && item.isSelectable() ) {
5170                         this.pressItem( item );
5171                         this.selecting = item;
5172                         this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
5173                         this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler, true );
5174                 }
5175         }
5176         return false;
5180  * Handle mouse up events.
5182  * @private
5183  * @param {MouseEvent} e Mouse up event
5184  */
5185 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
5186         var item;
5188         this.togglePressed( false );
5189         if ( !this.selecting ) {
5190                 item = this.getTargetItem( e );
5191                 if ( item && item.isSelectable() ) {
5192                         this.selecting = item;
5193                 }
5194         }
5195         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT && this.selecting ) {
5196                 this.pressItem( null );
5197                 this.chooseItem( this.selecting );
5198                 this.selecting = null;
5199         }
5201         this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
5202         this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler, true );
5204         return false;
5208  * Handle mouse move events.
5210  * @private
5211  * @param {MouseEvent} e Mouse move event
5212  */
5213 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
5214         var item;
5216         if ( !this.isDisabled() && this.pressed ) {
5217                 item = this.getTargetItem( e );
5218                 if ( item && item !== this.selecting && item.isSelectable() ) {
5219                         this.pressItem( item );
5220                         this.selecting = item;
5221                 }
5222         }
5226  * Handle mouse over events.
5228  * @private
5229  * @param {jQuery.Event} e Mouse over event
5230  */
5231 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
5232         var item;
5233         if ( this.blockMouseOverEvents ) {
5234                 return;
5235         }
5236         if ( !this.isDisabled() ) {
5237                 item = this.getTargetItem( e );
5238                 this.highlightItem( item && item.isHighlightable() ? item : null );
5239         }
5240         return false;
5244  * Handle mouse leave events.
5246  * @private
5247  * @param {jQuery.Event} e Mouse over event
5248  */
5249 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
5250         if ( !this.isDisabled() ) {
5251                 this.highlightItem( null );
5252         }
5253         return false;
5257  * Handle key down events.
5259  * @protected
5260  * @param {KeyboardEvent} e Key down event
5261  */
5262 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
5263         var nextItem,
5264                 handled = false,
5265                 currentItem = this.getHighlightedItem() || this.getSelectedItem();
5267         if ( !this.isDisabled() && this.isVisible() ) {
5268                 switch ( e.keyCode ) {
5269                         case OO.ui.Keys.ENTER:
5270                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
5271                                         // Was only highlighted, now let's select it. No-op if already selected.
5272                                         this.chooseItem( currentItem );
5273                                         handled = true;
5274                                 }
5275                                 break;
5276                         case OO.ui.Keys.UP:
5277                         case OO.ui.Keys.LEFT:
5278                                 this.clearKeyPressBuffer();
5279                                 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
5280                                 handled = true;
5281                                 break;
5282                         case OO.ui.Keys.DOWN:
5283                         case OO.ui.Keys.RIGHT:
5284                                 this.clearKeyPressBuffer();
5285                                 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
5286                                 handled = true;
5287                                 break;
5288                         case OO.ui.Keys.ESCAPE:
5289                         case OO.ui.Keys.TAB:
5290                                 if ( currentItem && currentItem.constructor.static.highlightable ) {
5291                                         currentItem.setHighlighted( false );
5292                                 }
5293                                 this.unbindKeyDownListener();
5294                                 this.unbindKeyPressListener();
5295                                 // Don't prevent tabbing away / defocusing
5296                                 handled = false;
5297                                 break;
5298                 }
5300                 if ( nextItem ) {
5301                         if ( nextItem.constructor.static.highlightable ) {
5302                                 this.highlightItem( nextItem );
5303                         } else {
5304                                 this.chooseItem( nextItem );
5305                         }
5306                         this.scrollItemIntoView( nextItem );
5307                 }
5309                 if ( handled ) {
5310                         e.preventDefault();
5311                         e.stopPropagation();
5312                 }
5313         }
5317  * Bind key down listener.
5319  * @protected
5320  */
5321 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
5322         this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
5326  * Unbind key down listener.
5328  * @protected
5329  */
5330 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
5331         this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
5335  * Scroll item into view, preventing spurious mouse highlight actions from happening.
5337  * @param {OO.ui.OptionWidget} item Item to scroll into view
5338  */
5339 OO.ui.SelectWidget.prototype.scrollItemIntoView = function ( item ) {
5340         var widget = this;
5341         // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
5342         // and around 100-150 ms after it is finished.
5343         this.blockMouseOverEvents++;
5344         item.scrollElementIntoView().done( function () {
5345                 setTimeout( function () {
5346                         widget.blockMouseOverEvents--;
5347                 }, 200 );
5348         } );
5352  * Clear the key-press buffer
5354  * @protected
5355  */
5356 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
5357         if ( this.keyPressBufferTimer ) {
5358                 clearTimeout( this.keyPressBufferTimer );
5359                 this.keyPressBufferTimer = null;
5360         }
5361         this.keyPressBuffer = '';
5365  * Handle key press events.
5367  * @protected
5368  * @param {KeyboardEvent} e Key press event
5369  */
5370 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
5371         var c, filter, item;
5373         if ( !e.charCode ) {
5374                 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
5375                         this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
5376                         return false;
5377                 }
5378                 return;
5379         }
5380         if ( String.fromCodePoint ) {
5381                 c = String.fromCodePoint( e.charCode );
5382         } else {
5383                 c = String.fromCharCode( e.charCode );
5384         }
5386         if ( this.keyPressBufferTimer ) {
5387                 clearTimeout( this.keyPressBufferTimer );
5388         }
5389         this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
5391         item = this.getHighlightedItem() || this.getSelectedItem();
5393         if ( this.keyPressBuffer === c ) {
5394                 // Common (if weird) special case: typing "xxxx" will cycle through all
5395                 // the items beginning with "x".
5396                 if ( item ) {
5397                         item = this.getRelativeSelectableItem( item, 1 );
5398                 }
5399         } else {
5400                 this.keyPressBuffer += c;
5401         }
5403         filter = this.getItemMatcher( this.keyPressBuffer, false );
5404         if ( !item || !filter( item ) ) {
5405                 item = this.getRelativeSelectableItem( item, 1, filter );
5406         }
5407         if ( item ) {
5408                 if ( this.isVisible() && item.constructor.static.highlightable ) {
5409                         this.highlightItem( item );
5410                 } else {
5411                         this.chooseItem( item );
5412                 }
5413                 this.scrollItemIntoView( item );
5414         }
5416         e.preventDefault();
5417         e.stopPropagation();
5421  * Get a matcher for the specific string
5423  * @protected
5424  * @param {string} s String to match against items
5425  * @param {boolean} [exact=false] Only accept exact matches
5426  * @return {Function} function ( OO.ui.OptionItem ) => boolean
5427  */
5428 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
5429         var re;
5431         if ( s.normalize ) {
5432                 s = s.normalize();
5433         }
5434         s = exact ? s.trim() : s.replace( /^\s+/, '' );
5435         re = '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
5436         if ( exact ) {
5437                 re += '\\s*$';
5438         }
5439         re = new RegExp( re, 'i' );
5440         return function ( item ) {
5441                 var l = item.getLabel();
5442                 if ( typeof l !== 'string' ) {
5443                         l = item.$label.text();
5444                 }
5445                 if ( l.normalize ) {
5446                         l = l.normalize();
5447                 }
5448                 return re.test( l );
5449         };
5453  * Bind key press listener.
5455  * @protected
5456  */
5457 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
5458         this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
5462  * Unbind key down listener.
5464  * If you override this, be sure to call this.clearKeyPressBuffer() from your
5465  * implementation.
5467  * @protected
5468  */
5469 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
5470         this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
5471         this.clearKeyPressBuffer();
5475  * Visibility change handler
5477  * @protected
5478  * @param {boolean} visible
5479  */
5480 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
5481         if ( !visible ) {
5482                 this.clearKeyPressBuffer();
5483         }
5487  * Get the closest item to a jQuery.Event.
5489  * @private
5490  * @param {jQuery.Event} e
5491  * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5492  */
5493 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
5494         return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
5498  * Get selected item.
5500  * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5501  */
5502 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
5503         var i, len;
5505         for ( i = 0, len = this.items.length; i < len; i++ ) {
5506                 if ( this.items[ i ].isSelected() ) {
5507                         return this.items[ i ];
5508                 }
5509         }
5510         return null;
5514  * Get highlighted item.
5516  * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5517  */
5518 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
5519         var i, len;
5521         for ( i = 0, len = this.items.length; i < len; i++ ) {
5522                 if ( this.items[ i ].isHighlighted() ) {
5523                         return this.items[ i ];
5524                 }
5525         }
5526         return null;
5530  * Toggle pressed state.
5532  * Press is a state that occurs when a user mouses down on an item, but
5533  * has not yet let go of the mouse. The item may appear selected, but it will not be selected
5534  * until the user releases the mouse.
5536  * @param {boolean} pressed An option is being pressed
5537  */
5538 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
5539         if ( pressed === undefined ) {
5540                 pressed = !this.pressed;
5541         }
5542         if ( pressed !== this.pressed ) {
5543                 this.$element
5544                         .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
5545                         .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
5546                 this.pressed = pressed;
5547         }
5551  * Highlight an option. If the `item` param is omitted, no options will be highlighted
5552  * and any existing highlight will be removed. The highlight is mutually exclusive.
5554  * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
5555  * @fires highlight
5556  * @chainable
5557  */
5558 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
5559         var i, len, highlighted,
5560                 changed = false;
5562         for ( i = 0, len = this.items.length; i < len; i++ ) {
5563                 highlighted = this.items[ i ] === item;
5564                 if ( this.items[ i ].isHighlighted() !== highlighted ) {
5565                         this.items[ i ].setHighlighted( highlighted );
5566                         changed = true;
5567                 }
5568         }
5569         if ( changed ) {
5570                 this.emit( 'highlight', item );
5571         }
5573         return this;
5577  * Fetch an item by its label.
5579  * @param {string} label Label of the item to select.
5580  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5581  * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
5582  */
5583 OO.ui.SelectWidget.prototype.getItemFromLabel = function ( label, prefix ) {
5584         var i, item, found,
5585                 len = this.items.length,
5586                 filter = this.getItemMatcher( label, true );
5588         for ( i = 0; i < len; i++ ) {
5589                 item = this.items[ i ];
5590                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
5591                         return item;
5592                 }
5593         }
5595         if ( prefix ) {
5596                 found = null;
5597                 filter = this.getItemMatcher( label, false );
5598                 for ( i = 0; i < len; i++ ) {
5599                         item = this.items[ i ];
5600                         if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
5601                                 if ( found ) {
5602                                         return null;
5603                                 }
5604                                 found = item;
5605                         }
5606                 }
5607                 if ( found ) {
5608                         return found;
5609                 }
5610         }
5612         return null;
5616  * Programmatically select an option by its label. If the item does not exist,
5617  * all options will be deselected.
5619  * @param {string} [label] Label of the item to select.
5620  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5621  * @fires select
5622  * @chainable
5623  */
5624 OO.ui.SelectWidget.prototype.selectItemByLabel = function ( label, prefix ) {
5625         var itemFromLabel = this.getItemFromLabel( label, !!prefix );
5626         if ( label === undefined || !itemFromLabel ) {
5627                 return this.selectItem();
5628         }
5629         return this.selectItem( itemFromLabel );
5633  * Programmatically select an option by its data. If the `data` parameter is omitted,
5634  * or if the item does not exist, all options will be deselected.
5636  * @param {Object|string} [data] Value of the item to select, omit to deselect all
5637  * @fires select
5638  * @chainable
5639  */
5640 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
5641         var itemFromData = this.getItemFromData( data );
5642         if ( data === undefined || !itemFromData ) {
5643                 return this.selectItem();
5644         }
5645         return this.selectItem( itemFromData );
5649  * Programmatically select an option by its reference. If the `item` parameter is omitted,
5650  * all options will be deselected.
5652  * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
5653  * @fires select
5654  * @chainable
5655  */
5656 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
5657         var i, len, selected,
5658                 changed = false;
5660         for ( i = 0, len = this.items.length; i < len; i++ ) {
5661                 selected = this.items[ i ] === item;
5662                 if ( this.items[ i ].isSelected() !== selected ) {
5663                         this.items[ i ].setSelected( selected );
5664                         changed = true;
5665                 }
5666         }
5667         if ( changed ) {
5668                 this.emit( 'select', item );
5669         }
5671         return this;
5675  * Press an item.
5677  * Press is a state that occurs when a user mouses down on an item, but has not
5678  * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
5679  * releases the mouse.
5681  * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
5682  * @fires press
5683  * @chainable
5684  */
5685 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
5686         var i, len, pressed,
5687                 changed = false;
5689         for ( i = 0, len = this.items.length; i < len; i++ ) {
5690                 pressed = this.items[ i ] === item;
5691                 if ( this.items[ i ].isPressed() !== pressed ) {
5692                         this.items[ i ].setPressed( pressed );
5693                         changed = true;
5694                 }
5695         }
5696         if ( changed ) {
5697                 this.emit( 'press', item );
5698         }
5700         return this;
5704  * Choose an item.
5706  * Note that ‘choose’ should never be modified programmatically. A user can choose
5707  * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
5708  * use the #selectItem method.
5710  * This method is identical to #selectItem, but may vary in subclasses that take additional action
5711  * when users choose an item with the keyboard or mouse.
5713  * @param {OO.ui.OptionWidget} item Item to choose
5714  * @fires choose
5715  * @chainable
5716  */
5717 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
5718         if ( item ) {
5719                 this.selectItem( item );
5720                 this.emit( 'choose', item );
5721         }
5723         return this;
5727  * Get an option by its position relative to the specified item (or to the start of the option array,
5728  * if item is `null`). The direction in which to search through the option array is specified with a
5729  * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
5730  * `null` if there are no options in the array.
5732  * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
5733  * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
5734  * @param {Function} [filter] Only consider items for which this function returns
5735  *  true. Function takes an OO.ui.OptionWidget and returns a boolean.
5736  * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
5737  */
5738 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
5739         var currentIndex, nextIndex, i,
5740                 increase = direction > 0 ? 1 : -1,
5741                 len = this.items.length;
5743         if ( item instanceof OO.ui.OptionWidget ) {
5744                 currentIndex = this.items.indexOf( item );
5745                 nextIndex = ( currentIndex + increase + len ) % len;
5746         } else {
5747                 // If no item is selected and moving forward, start at the beginning.
5748                 // If moving backward, start at the end.
5749                 nextIndex = direction > 0 ? 0 : len - 1;
5750         }
5752         for ( i = 0; i < len; i++ ) {
5753                 item = this.items[ nextIndex ];
5754                 if (
5755                         item instanceof OO.ui.OptionWidget && item.isSelectable() &&
5756                         ( !filter || filter( item ) )
5757                 ) {
5758                         return item;
5759                 }
5760                 nextIndex = ( nextIndex + increase + len ) % len;
5761         }
5762         return null;
5766  * Get the next selectable item or `null` if there are no selectable items.
5767  * Disabled options and menu-section markers and breaks are not selectable.
5769  * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
5770  */
5771 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
5772         return this.getRelativeSelectableItem( null, 1 );
5776  * Add an array of options to the select. Optionally, an index number can be used to
5777  * specify an insertion point.
5779  * @param {OO.ui.OptionWidget[]} items Items to add
5780  * @param {number} [index] Index to insert items after
5781  * @fires add
5782  * @chainable
5783  */
5784 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
5785         // Mixin method
5786         OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
5788         // Always provide an index, even if it was omitted
5789         this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
5791         return this;
5795  * Remove the specified array of options from the select. Options will be detached
5796  * from the DOM, not removed, so they can be reused later. To remove all options from
5797  * the select, you may wish to use the #clearItems method instead.
5799  * @param {OO.ui.OptionWidget[]} items Items to remove
5800  * @fires remove
5801  * @chainable
5802  */
5803 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
5804         var i, len, item;
5806         // Deselect items being removed
5807         for ( i = 0, len = items.length; i < len; i++ ) {
5808                 item = items[ i ];
5809                 if ( item.isSelected() ) {
5810                         this.selectItem( null );
5811                 }
5812         }
5814         // Mixin method
5815         OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
5817         this.emit( 'remove', items );
5819         return this;
5823  * Clear all options from the select. Options will be detached from the DOM, not removed,
5824  * so that they can be reused later. To remove a subset of options from the select, use
5825  * the #removeItems method.
5827  * @fires remove
5828  * @chainable
5829  */
5830 OO.ui.SelectWidget.prototype.clearItems = function () {
5831         var items = this.items.slice();
5833         // Mixin method
5834         OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
5836         // Clear selection
5837         this.selectItem( null );
5839         this.emit( 'remove', items );
5841         return this;
5845  * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
5846  * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
5847  * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
5848  * options. For more information about options and selects, please see the
5849  * [OOjs UI documentation on MediaWiki][1].
5851  *     @example
5852  *     // Decorated options in a select widget
5853  *     var select = new OO.ui.SelectWidget( {
5854  *         items: [
5855  *             new OO.ui.DecoratedOptionWidget( {
5856  *                 data: 'a',
5857  *                 label: 'Option with icon',
5858  *                 icon: 'help'
5859  *             } ),
5860  *             new OO.ui.DecoratedOptionWidget( {
5861  *                 data: 'b',
5862  *                 label: 'Option with indicator',
5863  *                 indicator: 'next'
5864  *             } )
5865  *         ]
5866  *     } );
5867  *     $( 'body' ).append( select.$element );
5869  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5871  * @class
5872  * @extends OO.ui.OptionWidget
5873  * @mixins OO.ui.mixin.IconElement
5874  * @mixins OO.ui.mixin.IndicatorElement
5876  * @constructor
5877  * @param {Object} [config] Configuration options
5878  */
5879 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
5880         // Parent constructor
5881         OO.ui.DecoratedOptionWidget.parent.call( this, config );
5883         // Mixin constructors
5884         OO.ui.mixin.IconElement.call( this, config );
5885         OO.ui.mixin.IndicatorElement.call( this, config );
5887         // Initialization
5888         this.$element
5889                 .addClass( 'oo-ui-decoratedOptionWidget' )
5890                 .prepend( this.$icon )
5891                 .append( this.$indicator );
5894 /* Setup */
5896 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
5897 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
5898 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
5901  * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
5902  * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
5903  * the [OOjs UI documentation on MediaWiki] [1] for more information.
5905  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
5907  * @class
5908  * @extends OO.ui.DecoratedOptionWidget
5910  * @constructor
5911  * @param {Object} [config] Configuration options
5912  */
5913 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
5914         // Configuration initialization
5915         config = $.extend( { icon: 'check' }, config );
5917         // Parent constructor
5918         OO.ui.MenuOptionWidget.parent.call( this, config );
5920         // Initialization
5921         this.$element
5922                 .attr( 'role', 'menuitem' )
5923                 .addClass( 'oo-ui-menuOptionWidget' );
5926 /* Setup */
5928 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
5930 /* Static Properties */
5932 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
5935  * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
5936  * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
5938  *     @example
5939  *     var myDropdown = new OO.ui.DropdownWidget( {
5940  *         menu: {
5941  *             items: [
5942  *                 new OO.ui.MenuSectionOptionWidget( {
5943  *                     label: 'Dogs'
5944  *                 } ),
5945  *                 new OO.ui.MenuOptionWidget( {
5946  *                     data: 'corgi',
5947  *                     label: 'Welsh Corgi'
5948  *                 } ),
5949  *                 new OO.ui.MenuOptionWidget( {
5950  *                     data: 'poodle',
5951  *                     label: 'Standard Poodle'
5952  *                 } ),
5953  *                 new OO.ui.MenuSectionOptionWidget( {
5954  *                     label: 'Cats'
5955  *                 } ),
5956  *                 new OO.ui.MenuOptionWidget( {
5957  *                     data: 'lion',
5958  *                     label: 'Lion'
5959  *                 } )
5960  *             ]
5961  *         }
5962  *     } );
5963  *     $( 'body' ).append( myDropdown.$element );
5965  * @class
5966  * @extends OO.ui.DecoratedOptionWidget
5968  * @constructor
5969  * @param {Object} [config] Configuration options
5970  */
5971 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
5972         // Parent constructor
5973         OO.ui.MenuSectionOptionWidget.parent.call( this, config );
5975         // Initialization
5976         this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
5979 /* Setup */
5981 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
5983 /* Static Properties */
5985 OO.ui.MenuSectionOptionWidget.static.selectable = false;
5987 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
5990  * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
5991  * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
5992  * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
5993  * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
5994  * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
5995  * and customized to be opened, closed, and displayed as needed.
5997  * By default, menus are clipped to the visible viewport and are not visible when a user presses the
5998  * mouse outside the menu.
6000  * Menus also have support for keyboard interaction:
6002  * - Enter/Return key: choose and select a menu option
6003  * - Up-arrow key: highlight the previous menu option
6004  * - Down-arrow key: highlight the next menu option
6005  * - Esc key: hide the menu
6007  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6008  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6010  * @class
6011  * @extends OO.ui.SelectWidget
6012  * @mixins OO.ui.mixin.ClippableElement
6014  * @constructor
6015  * @param {Object} [config] Configuration options
6016  * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
6017  *  the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
6018  *  and {@link OO.ui.mixin.LookupElement LookupElement}
6019  * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
6020  *  the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
6021  * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
6022  *  anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
6023  *  that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
6024  *  that button, unless the button (or its parent widget) is passed in here.
6025  * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
6026  * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
6027  */
6028 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
6029         // Configuration initialization
6030         config = config || {};
6032         // Parent constructor
6033         OO.ui.MenuSelectWidget.parent.call( this, config );
6035         // Mixin constructors
6036         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
6038         // Properties
6039         this.autoHide = config.autoHide === undefined || !!config.autoHide;
6040         this.filterFromInput = !!config.filterFromInput;
6041         this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
6042         this.$widget = config.widget ? config.widget.$element : null;
6043         this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
6044         this.onInputEditHandler = OO.ui.debounce( this.updateItemVisibility.bind( this ), 100 );
6046         // Initialization
6047         this.$element
6048                 .addClass( 'oo-ui-menuSelectWidget' )
6049                 .attr( 'role', 'menu' );
6051         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
6052         // that reference properties not initialized at that time of parent class construction
6053         // TODO: Find a better way to handle post-constructor setup
6054         this.visible = false;
6055         this.$element.addClass( 'oo-ui-element-hidden' );
6058 /* Setup */
6060 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
6061 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
6063 /* Methods */
6066  * Handles document mouse down events.
6068  * @protected
6069  * @param {MouseEvent} e Mouse down event
6070  */
6071 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
6072         if (
6073                 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
6074                 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
6075         ) {
6076                 this.toggle( false );
6077         }
6081  * @inheritdoc
6082  */
6083 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
6084         var currentItem = this.getHighlightedItem() || this.getSelectedItem();
6086         if ( !this.isDisabled() && this.isVisible() ) {
6087                 switch ( e.keyCode ) {
6088                         case OO.ui.Keys.LEFT:
6089                         case OO.ui.Keys.RIGHT:
6090                                 // Do nothing if a text field is associated, arrow keys will be handled natively
6091                                 if ( !this.$input ) {
6092                                         OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
6093                                 }
6094                                 break;
6095                         case OO.ui.Keys.ESCAPE:
6096                         case OO.ui.Keys.TAB:
6097                                 if ( currentItem ) {
6098                                         currentItem.setHighlighted( false );
6099                                 }
6100                                 this.toggle( false );
6101                                 // Don't prevent tabbing away, prevent defocusing
6102                                 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
6103                                         e.preventDefault();
6104                                         e.stopPropagation();
6105                                 }
6106                                 break;
6107                         default:
6108                                 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
6109                                 return;
6110                 }
6111         }
6115  * Update menu item visibility after input changes.
6117  * @protected
6118  */
6119 OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
6120         var i, item,
6121                 len = this.items.length,
6122                 showAll = !this.isVisible(),
6123                 filter = showAll ? null : this.getItemMatcher( this.$input.val() );
6125         for ( i = 0; i < len; i++ ) {
6126                 item = this.items[ i ];
6127                 if ( item instanceof OO.ui.OptionWidget ) {
6128                         item.toggle( showAll || filter( item ) );
6129                 }
6130         }
6132         // Reevaluate clipping
6133         this.clip();
6137  * @inheritdoc
6138  */
6139 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
6140         if ( this.$input ) {
6141                 this.$input.on( 'keydown', this.onKeyDownHandler );
6142         } else {
6143                 OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
6144         }
6148  * @inheritdoc
6149  */
6150 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
6151         if ( this.$input ) {
6152                 this.$input.off( 'keydown', this.onKeyDownHandler );
6153         } else {
6154                 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
6155         }
6159  * @inheritdoc
6160  */
6161 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
6162         if ( this.$input ) {
6163                 if ( this.filterFromInput ) {
6164                         this.$input.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
6165                 }
6166         } else {
6167                 OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
6168         }
6172  * @inheritdoc
6173  */
6174 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
6175         if ( this.$input ) {
6176                 if ( this.filterFromInput ) {
6177                         this.$input.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
6178                         this.updateItemVisibility();
6179                 }
6180         } else {
6181                 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
6182         }
6186  * Choose an item.
6188  * When a user chooses an item, the menu is closed.
6190  * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
6191  * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
6193  * @param {OO.ui.OptionWidget} item Item to choose
6194  * @chainable
6195  */
6196 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
6197         OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
6198         this.toggle( false );
6199         return this;
6203  * @inheritdoc
6204  */
6205 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
6206         // Parent method
6207         OO.ui.MenuSelectWidget.parent.prototype.addItems.call( this, items, index );
6209         // Reevaluate clipping
6210         this.clip();
6212         return this;
6216  * @inheritdoc
6217  */
6218 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
6219         // Parent method
6220         OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
6222         // Reevaluate clipping
6223         this.clip();
6225         return this;
6229  * @inheritdoc
6230  */
6231 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
6232         // Parent method
6233         OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
6235         // Reevaluate clipping
6236         this.clip();
6238         return this;
6242  * @inheritdoc
6243  */
6244 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
6245         var change;
6247         visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
6248         change = visible !== this.isVisible();
6250         // Parent method
6251         OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
6253         if ( change ) {
6254                 if ( visible ) {
6255                         this.bindKeyDownListener();
6256                         this.bindKeyPressListener();
6258                         this.toggleClipping( true );
6260                         if ( this.getSelectedItem() ) {
6261                                 this.getSelectedItem().scrollElementIntoView( { duration: 0 } );
6262                         }
6264                         // Auto-hide
6265                         if ( this.autoHide ) {
6266                                 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
6267                         }
6268                 } else {
6269                         this.unbindKeyDownListener();
6270                         this.unbindKeyPressListener();
6271                         this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
6272                         this.toggleClipping( false );
6273                 }
6274         }
6276         return this;
6280  * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
6281  * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
6282  * users can interact with it.
6284  * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6285  * OO.ui.DropdownInputWidget instead.
6287  *     @example
6288  *     // Example: A DropdownWidget with a menu that contains three options
6289  *     var dropDown = new OO.ui.DropdownWidget( {
6290  *         label: 'Dropdown menu: Select a menu option',
6291  *         menu: {
6292  *             items: [
6293  *                 new OO.ui.MenuOptionWidget( {
6294  *                     data: 'a',
6295  *                     label: 'First'
6296  *                 } ),
6297  *                 new OO.ui.MenuOptionWidget( {
6298  *                     data: 'b',
6299  *                     label: 'Second'
6300  *                 } ),
6301  *                 new OO.ui.MenuOptionWidget( {
6302  *                     data: 'c',
6303  *                     label: 'Third'
6304  *                 } )
6305  *             ]
6306  *         }
6307  *     } );
6309  *     $( 'body' ).append( dropDown.$element );
6311  *     dropDown.getMenu().selectItemByData( 'b' );
6313  *     dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
6315  * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
6317  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6319  * @class
6320  * @extends OO.ui.Widget
6321  * @mixins OO.ui.mixin.IconElement
6322  * @mixins OO.ui.mixin.IndicatorElement
6323  * @mixins OO.ui.mixin.LabelElement
6324  * @mixins OO.ui.mixin.TitledElement
6325  * @mixins OO.ui.mixin.TabIndexedElement
6327  * @constructor
6328  * @param {Object} [config] Configuration options
6329  * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
6330  * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
6331  *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6332  *  containing `<div>` and has a larger area. By default, the menu uses relative positioning.
6333  */
6334 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
6335         // Configuration initialization
6336         config = $.extend( { indicator: 'down' }, config );
6338         // Parent constructor
6339         OO.ui.DropdownWidget.parent.call( this, config );
6341         // Properties (must be set before TabIndexedElement constructor call)
6342         this.$handle = this.$( '<span>' );
6343         this.$overlay = config.$overlay || this.$element;
6345         // Mixin constructors
6346         OO.ui.mixin.IconElement.call( this, config );
6347         OO.ui.mixin.IndicatorElement.call( this, config );
6348         OO.ui.mixin.LabelElement.call( this, config );
6349         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
6350         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
6352         // Properties
6353         this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend( {
6354                 widget: this,
6355                 $container: this.$element
6356         }, config.menu ) );
6358         // Events
6359         this.$handle.on( {
6360                 click: this.onClick.bind( this ),
6361                 keydown: this.onKeyDown.bind( this ),
6362                 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
6363                 keypress: this.menu.onKeyPressHandler,
6364                 blur: this.menu.clearKeyPressBuffer.bind( this.menu )
6365         } );
6366         this.menu.connect( this, {
6367                 select: 'onMenuSelect',
6368                 toggle: 'onMenuToggle'
6369         } );
6371         // Initialization
6372         this.$handle
6373                 .addClass( 'oo-ui-dropdownWidget-handle' )
6374                 .append( this.$icon, this.$label, this.$indicator );
6375         this.$element
6376                 .addClass( 'oo-ui-dropdownWidget' )
6377                 .append( this.$handle );
6378         this.$overlay.append( this.menu.$element );
6381 /* Setup */
6383 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
6384 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
6385 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
6386 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
6387 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
6388 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
6390 /* Methods */
6393  * Get the menu.
6395  * @return {OO.ui.MenuSelectWidget} Menu of widget
6396  */
6397 OO.ui.DropdownWidget.prototype.getMenu = function () {
6398         return this.menu;
6402  * Handles menu select events.
6404  * @private
6405  * @param {OO.ui.MenuOptionWidget} item Selected menu item
6406  */
6407 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
6408         var selectedLabel;
6410         if ( !item ) {
6411                 this.setLabel( null );
6412                 return;
6413         }
6415         selectedLabel = item.getLabel();
6417         // If the label is a DOM element, clone it, because setLabel will append() it
6418         if ( selectedLabel instanceof jQuery ) {
6419                 selectedLabel = selectedLabel.clone();
6420         }
6422         this.setLabel( selectedLabel );
6426  * Handle menu toggle events.
6428  * @private
6429  * @param {boolean} isVisible Menu toggle event
6430  */
6431 OO.ui.DropdownWidget.prototype.onMenuToggle = function ( isVisible ) {
6432         this.$element.toggleClass( 'oo-ui-dropdownWidget-open', isVisible );
6436  * Handle mouse click events.
6438  * @private
6439  * @param {jQuery.Event} e Mouse click event
6440  */
6441 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
6442         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
6443                 this.menu.toggle();
6444         }
6445         return false;
6449  * Handle key down events.
6451  * @private
6452  * @param {jQuery.Event} e Key down event
6453  */
6454 OO.ui.DropdownWidget.prototype.onKeyDown = function ( e ) {
6455         if (
6456                 !this.isDisabled() &&
6457                 (
6458                         e.which === OO.ui.Keys.ENTER ||
6459                         (
6460                                 !this.menu.isVisible() &&
6461                                 (
6462                                         e.which === OO.ui.Keys.SPACE ||
6463                                         e.which === OO.ui.Keys.UP ||
6464                                         e.which === OO.ui.Keys.DOWN
6465                                 )
6466                         )
6467                 )
6468         ) {
6469                 this.menu.toggle();
6470                 return false;
6471         }
6475  * RadioOptionWidget is an option widget that looks like a radio button.
6476  * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
6477  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6479  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6481  * @class
6482  * @extends OO.ui.OptionWidget
6484  * @constructor
6485  * @param {Object} [config] Configuration options
6486  */
6487 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
6488         // Configuration initialization
6489         config = config || {};
6491         // Properties (must be done before parent constructor which calls #setDisabled)
6492         this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
6494         // Parent constructor
6495         OO.ui.RadioOptionWidget.parent.call( this, config );
6497         // Initialization
6498         // Remove implicit role, we're handling it ourselves
6499         this.radio.$input.attr( 'role', 'presentation' );
6500         this.$element
6501                 .addClass( 'oo-ui-radioOptionWidget' )
6502                 .attr( 'role', 'radio' )
6503                 .attr( 'aria-checked', 'false' )
6504                 .removeAttr( 'aria-selected' )
6505                 .prepend( this.radio.$element );
6508 /* Setup */
6510 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
6512 /* Static Properties */
6514 OO.ui.RadioOptionWidget.static.highlightable = false;
6516 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
6518 OO.ui.RadioOptionWidget.static.pressable = false;
6520 OO.ui.RadioOptionWidget.static.tagName = 'label';
6522 /* Methods */
6525  * @inheritdoc
6526  */
6527 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
6528         OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
6530         this.radio.setSelected( state );
6531         this.$element
6532                 .attr( 'aria-checked', state.toString() )
6533                 .removeAttr( 'aria-selected' );
6535         return this;
6539  * @inheritdoc
6540  */
6541 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
6542         OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
6544         this.radio.setDisabled( this.isDisabled() );
6546         return this;
6550  * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
6551  * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
6552  * an interface for adding, removing and selecting options.
6553  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6555  * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6556  * OO.ui.RadioSelectInputWidget instead.
6558  *     @example
6559  *     // A RadioSelectWidget with RadioOptions.
6560  *     var option1 = new OO.ui.RadioOptionWidget( {
6561  *         data: 'a',
6562  *         label: 'Selected radio option'
6563  *     } );
6565  *     var option2 = new OO.ui.RadioOptionWidget( {
6566  *         data: 'b',
6567  *         label: 'Unselected radio option'
6568  *     } );
6570  *     var radioSelect=new OO.ui.RadioSelectWidget( {
6571  *         items: [ option1, option2 ]
6572  *      } );
6574  *     // Select 'option 1' using the RadioSelectWidget's selectItem() method.
6575  *     radioSelect.selectItem( option1 );
6577  *     $( 'body' ).append( radioSelect.$element );
6579  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6582  * @class
6583  * @extends OO.ui.SelectWidget
6584  * @mixins OO.ui.mixin.TabIndexedElement
6586  * @constructor
6587  * @param {Object} [config] Configuration options
6588  */
6589 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
6590         // Parent constructor
6591         OO.ui.RadioSelectWidget.parent.call( this, config );
6593         // Mixin constructors
6594         OO.ui.mixin.TabIndexedElement.call( this, config );
6596         // Events
6597         this.$element.on( {
6598                 focus: this.bindKeyDownListener.bind( this ),
6599                 blur: this.unbindKeyDownListener.bind( this )
6600         } );
6602         // Initialization
6603         this.$element
6604                 .addClass( 'oo-ui-radioSelectWidget' )
6605                 .attr( 'role', 'radiogroup' );
6608 /* Setup */
6610 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
6611 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
6614  * MultioptionWidgets are special elements that can be selected and configured with data. The
6615  * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
6616  * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6617  * and examples, please see the [OOjs UI documentation on MediaWiki][1].
6619  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Multioptions
6621  * @class
6622  * @extends OO.ui.Widget
6623  * @mixins OO.ui.mixin.ItemWidget
6624  * @mixins OO.ui.mixin.LabelElement
6626  * @constructor
6627  * @param {Object} [config] Configuration options
6628  * @cfg {boolean} [selected=false] Whether the option is initially selected
6629  */
6630 OO.ui.MultioptionWidget = function OoUiMultioptionWidget( config ) {
6631         // Configuration initialization
6632         config = config || {};
6634         // Parent constructor
6635         OO.ui.MultioptionWidget.parent.call( this, config );
6637         // Mixin constructors
6638         OO.ui.mixin.ItemWidget.call( this );
6639         OO.ui.mixin.LabelElement.call( this, config );
6641         // Properties
6642         this.selected = null;
6644         // Initialization
6645         this.$element
6646                 .addClass( 'oo-ui-multioptionWidget' )
6647                 .append( this.$label );
6648         this.setSelected( config.selected );
6651 /* Setup */
6653 OO.inheritClass( OO.ui.MultioptionWidget, OO.ui.Widget );
6654 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.ItemWidget );
6655 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.LabelElement );
6657 /* Events */
6660  * @event change
6662  * A change event is emitted when the selected state of the option changes.
6664  * @param {boolean} selected Whether the option is now selected
6665  */
6667 /* Methods */
6670  * Check if the option is selected.
6672  * @return {boolean} Item is selected
6673  */
6674 OO.ui.MultioptionWidget.prototype.isSelected = function () {
6675         return this.selected;
6679  * Set the option’s selected state. In general, all modifications to the selection
6680  * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6681  * method instead of this method.
6683  * @param {boolean} [state=false] Select option
6684  * @chainable
6685  */
6686 OO.ui.MultioptionWidget.prototype.setSelected = function ( state ) {
6687         state = !!state;
6688         if ( this.selected !== state ) {
6689                 this.selected = state;
6690                 this.emit( 'change', state );
6691                 this.$element.toggleClass( 'oo-ui-multioptionWidget-selected', state );
6692         }
6693         return this;
6697  * MultiselectWidget allows selecting multiple options from a list.
6699  * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
6701  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6703  * @class
6704  * @abstract
6705  * @extends OO.ui.Widget
6706  * @mixins OO.ui.mixin.GroupWidget
6708  * @constructor
6709  * @param {Object} [config] Configuration options
6710  * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
6711  */
6712 OO.ui.MultiselectWidget = function OoUiMultiselectWidget( config ) {
6713         // Parent constructor
6714         OO.ui.MultiselectWidget.parent.call( this, config );
6716         // Configuration initialization
6717         config = config || {};
6719         // Mixin constructors
6720         OO.ui.mixin.GroupWidget.call( this, config );
6722         // Events
6723         this.aggregate( { change: 'select' } );
6724         // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
6725         // by GroupElement only when items are added/removed
6726         this.connect( this, { select: [ 'emit', 'change' ] } );
6728         // Initialization
6729         if ( config.items ) {
6730                 this.addItems( config.items );
6731         }
6732         this.$group.addClass( 'oo-ui-multiselectWidget-group' );
6733         this.$element.addClass( 'oo-ui-multiselectWidget' )
6734                 .append( this.$group );
6737 /* Setup */
6739 OO.inheritClass( OO.ui.MultiselectWidget, OO.ui.Widget );
6740 OO.mixinClass( OO.ui.MultiselectWidget, OO.ui.mixin.GroupWidget );
6742 /* Events */
6745  * @event change
6747  * A change event is emitted when the set of items changes, or an item is selected or deselected.
6748  */
6751  * @event select
6753  * A select event is emitted when an item is selected or deselected.
6754  */
6756 /* Methods */
6759  * Get options that are selected.
6761  * @return {OO.ui.MultioptionWidget[]} Selected options
6762  */
6763 OO.ui.MultiselectWidget.prototype.getSelectedItems = function () {
6764         return this.items.filter( function ( item ) {
6765                 return item.isSelected();
6766         } );
6770  * Get the data of options that are selected.
6772  * @return {Object[]|string[]} Values of selected options
6773  */
6774 OO.ui.MultiselectWidget.prototype.getSelectedItemsData = function () {
6775         return this.getSelectedItems().map( function ( item ) {
6776                 return item.data;
6777         } );
6781  * Select options by reference. Options not mentioned in the `items` array will be deselected.
6783  * @param {OO.ui.MultioptionWidget[]} items Items to select
6784  * @chainable
6785  */
6786 OO.ui.MultiselectWidget.prototype.selectItems = function ( items ) {
6787         this.items.forEach( function ( item ) {
6788                 var selected = items.indexOf( item ) !== -1;
6789                 item.setSelected( selected );
6790         } );
6791         return this;
6795  * Select items by their data. Options not mentioned in the `datas` array will be deselected.
6797  * @param {Object[]|string[]} datas Values of items to select
6798  * @chainable
6799  */
6800 OO.ui.MultiselectWidget.prototype.selectItemsByData = function ( datas ) {
6801         var items,
6802                 widget = this;
6803         items = datas.map( function ( data ) {
6804                 return widget.getItemFromData( data );
6805         } );
6806         this.selectItems( items );
6807         return this;
6811  * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
6812  * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
6813  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6815  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6817  * @class
6818  * @extends OO.ui.MultioptionWidget
6820  * @constructor
6821  * @param {Object} [config] Configuration options
6822  */
6823 OO.ui.CheckboxMultioptionWidget = function OoUiCheckboxMultioptionWidget( config ) {
6824         // Configuration initialization
6825         config = config || {};
6827         // Properties (must be done before parent constructor which calls #setDisabled)
6828         this.checkbox = new OO.ui.CheckboxInputWidget();
6830         // Parent constructor
6831         OO.ui.CheckboxMultioptionWidget.parent.call( this, config );
6833         // Events
6834         this.checkbox.on( 'change', this.onCheckboxChange.bind( this ) );
6835         this.$element.on( 'keydown', this.onKeyDown.bind( this ) );
6837         // Initialization
6838         this.$element
6839                 .addClass( 'oo-ui-checkboxMultioptionWidget' )
6840                 .prepend( this.checkbox.$element );
6843 /* Setup */
6845 OO.inheritClass( OO.ui.CheckboxMultioptionWidget, OO.ui.MultioptionWidget );
6847 /* Static Properties */
6849 OO.ui.CheckboxMultioptionWidget.static.tagName = 'label';
6851 /* Methods */
6854  * Handle checkbox selected state change.
6856  * @private
6857  */
6858 OO.ui.CheckboxMultioptionWidget.prototype.onCheckboxChange = function () {
6859         this.setSelected( this.checkbox.isSelected() );
6863  * @inheritdoc
6864  */
6865 OO.ui.CheckboxMultioptionWidget.prototype.setSelected = function ( state ) {
6866         OO.ui.CheckboxMultioptionWidget.parent.prototype.setSelected.call( this, state );
6867         this.checkbox.setSelected( state );
6868         return this;
6872  * @inheritdoc
6873  */
6874 OO.ui.CheckboxMultioptionWidget.prototype.setDisabled = function ( disabled ) {
6875         OO.ui.CheckboxMultioptionWidget.parent.prototype.setDisabled.call( this, disabled );
6876         this.checkbox.setDisabled( this.isDisabled() );
6877         return this;
6881  * Focus the widget.
6882  */
6883 OO.ui.CheckboxMultioptionWidget.prototype.focus = function () {
6884         this.checkbox.focus();
6888  * Handle key down events.
6890  * @protected
6891  * @param {jQuery.Event} e
6892  */
6893 OO.ui.CheckboxMultioptionWidget.prototype.onKeyDown = function ( e ) {
6894         var
6895                 element = this.getElementGroup(),
6896                 nextItem;
6898         if ( e.keyCode === OO.ui.Keys.LEFT || e.keyCode === OO.ui.Keys.UP ) {
6899                 nextItem = element.getRelativeFocusableItem( this, -1 );
6900         } else if ( e.keyCode === OO.ui.Keys.RIGHT || e.keyCode === OO.ui.Keys.DOWN ) {
6901                 nextItem = element.getRelativeFocusableItem( this, 1 );
6902         }
6904         if ( nextItem ) {
6905                 e.preventDefault();
6906                 nextItem.focus();
6907         }
6911  * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
6912  * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
6913  * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
6914  * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6916  * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6917  * OO.ui.CheckboxMultiselectInputWidget instead.
6919  *     @example
6920  *     // A CheckboxMultiselectWidget with CheckboxMultioptions.
6921  *     var option1 = new OO.ui.CheckboxMultioptionWidget( {
6922  *         data: 'a',
6923  *         selected: true,
6924  *         label: 'Selected checkbox'
6925  *     } );
6927  *     var option2 = new OO.ui.CheckboxMultioptionWidget( {
6928  *         data: 'b',
6929  *         label: 'Unselected checkbox'
6930  *     } );
6932  *     var multiselect=new OO.ui.CheckboxMultiselectWidget( {
6933  *         items: [ option1, option2 ]
6934  *      } );
6936  *     $( 'body' ).append( multiselect.$element );
6938  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6940  * @class
6941  * @extends OO.ui.MultiselectWidget
6943  * @constructor
6944  * @param {Object} [config] Configuration options
6945  */
6946 OO.ui.CheckboxMultiselectWidget = function OoUiCheckboxMultiselectWidget( config ) {
6947         // Parent constructor
6948         OO.ui.CheckboxMultiselectWidget.parent.call( this, config );
6950         // Properties
6951         this.$lastClicked = null;
6953         // Events
6954         this.$group.on( 'click', this.onClick.bind( this ) );
6956         // Initialization
6957         this.$element
6958                 .addClass( 'oo-ui-checkboxMultiselectWidget' );
6961 /* Setup */
6963 OO.inheritClass( OO.ui.CheckboxMultiselectWidget, OO.ui.MultiselectWidget );
6965 /* Methods */
6968  * Get an option by its position relative to the specified item (or to the start of the option array,
6969  * if item is `null`). The direction in which to search through the option array is specified with a
6970  * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6971  * `null` if there are no options in the array.
6973  * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6974  * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6975  * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
6976  */
6977 OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function ( item, direction ) {
6978         var currentIndex, nextIndex, i,
6979                 increase = direction > 0 ? 1 : -1,
6980                 len = this.items.length;
6982         if ( item ) {
6983                 currentIndex = this.items.indexOf( item );
6984                 nextIndex = ( currentIndex + increase + len ) % len;
6985         } else {
6986                 // If no item is selected and moving forward, start at the beginning.
6987                 // If moving backward, start at the end.
6988                 nextIndex = direction > 0 ? 0 : len - 1;
6989         }
6991         for ( i = 0; i < len; i++ ) {
6992                 item = this.items[ nextIndex ];
6993                 if ( item && !item.isDisabled() ) {
6994                         return item;
6995                 }
6996                 nextIndex = ( nextIndex + increase + len ) % len;
6997         }
6998         return null;
7002  * Handle click events on checkboxes.
7004  * @param {jQuery.Event} e
7005  */
7006 OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
7007         var $options, lastClickedIndex, nowClickedIndex, i, direction, wasSelected, items,
7008                 $lastClicked = this.$lastClicked,
7009                 $nowClicked = $( e.target ).closest( '.oo-ui-checkboxMultioptionWidget' )
7010                         .not( '.oo-ui-widget-disabled' );
7012         // Allow selecting multiple options at once by Shift-clicking them
7013         if ( $lastClicked && $nowClicked.length && e.shiftKey ) {
7014                 $options = this.$group.find( '.oo-ui-checkboxMultioptionWidget' );
7015                 lastClickedIndex = $options.index( $lastClicked );
7016                 nowClickedIndex = $options.index( $nowClicked );
7017                 // If it's the same item, either the user is being silly, or it's a fake event generated by the
7018                 // browser. In either case we don't need custom handling.
7019                 if ( nowClickedIndex !== lastClickedIndex ) {
7020                         items = this.items;
7021                         wasSelected = items[ nowClickedIndex ].isSelected();
7022                         direction = nowClickedIndex > lastClickedIndex ? 1 : -1;
7024                         // This depends on the DOM order of the items and the order of the .items array being the same.
7025                         for ( i = lastClickedIndex; i !== nowClickedIndex; i += direction ) {
7026                                 if ( !items[ i ].isDisabled() ) {
7027                                         items[ i ].setSelected( !wasSelected );
7028                                 }
7029                         }
7030                         // For the now-clicked element, use immediate timeout to allow the browser to do its own
7031                         // handling first, then set our value. The order in which events happen is different for
7032                         // clicks on the <input> and on the <label> and there are additional fake clicks fired for
7033                         // non-click actions that change the checkboxes.
7034                         e.preventDefault();
7035                         setTimeout( function () {
7036                                 if ( !items[ nowClickedIndex ].isDisabled() ) {
7037                                         items[ nowClickedIndex ].setSelected( !wasSelected );
7038                                 }
7039                         } );
7040                 }
7041         }
7043         if ( $nowClicked.length ) {
7044                 this.$lastClicked = $nowClicked;
7045         }
7049  * Element that will stick under a specified container, even when it is inserted elsewhere in the
7050  * document (for example, in a OO.ui.Window's $overlay).
7052  * The elements's position is automatically calculated and maintained when window is resized or the
7053  * page is scrolled. If you reposition the container manually, you have to call #position to make
7054  * sure the element is still placed correctly.
7056  * As positioning is only possible when both the element and the container are attached to the DOM
7057  * and visible, it's only done after you call #togglePositioning. You might want to do this inside
7058  * the #toggle method to display a floating popup, for example.
7060  * @abstract
7061  * @class
7063  * @constructor
7064  * @param {Object} [config] Configuration options
7065  * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
7066  * @cfg {jQuery} [$floatableContainer] Node to position below
7067  */
7068 OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
7069         // Configuration initialization
7070         config = config || {};
7072         // Properties
7073         this.$floatable = null;
7074         this.$floatableContainer = null;
7075         this.$floatableWindow = null;
7076         this.$floatableClosestScrollable = null;
7077         this.onFloatableScrollHandler = this.position.bind( this );
7078         this.onFloatableWindowResizeHandler = this.position.bind( this );
7080         // Initialization
7081         this.setFloatableContainer( config.$floatableContainer );
7082         this.setFloatableElement( config.$floatable || this.$element );
7085 /* Methods */
7088  * Set floatable element.
7090  * If an element is already set, it will be cleaned up before setting up the new element.
7092  * @param {jQuery} $floatable Element to make floatable
7093  */
7094 OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
7095         if ( this.$floatable ) {
7096                 this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
7097                 this.$floatable.css( { left: '', top: '' } );
7098         }
7100         this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
7101         this.position();
7105  * Set floatable container.
7107  * The element will be always positioned under the specified container.
7109  * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
7110  */
7111 OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
7112         this.$floatableContainer = $floatableContainer;
7113         if ( this.$floatable ) {
7114                 this.position();
7115         }
7119  * Toggle positioning.
7121  * Do not turn positioning on until after the element is attached to the DOM and visible.
7123  * @param {boolean} [positioning] Enable positioning, omit to toggle
7124  * @chainable
7125  */
7126 OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
7127         var closestScrollableOfContainer, closestScrollableOfFloatable;
7129         positioning = positioning === undefined ? !this.positioning : !!positioning;
7131         if ( this.positioning !== positioning ) {
7132                 this.positioning = positioning;
7134                 closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
7135                 closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
7136                 this.needsCustomPosition = closestScrollableOfContainer !== closestScrollableOfFloatable;
7137                 // If the scrollable is the root, we have to listen to scroll events
7138                 // on the window because of browser inconsistencies.
7139                 if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
7140                         closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
7141                 }
7143                 if ( positioning ) {
7144                         this.$floatableWindow = $( this.getElementWindow() );
7145                         this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
7147                         this.$floatableClosestScrollable = $( closestScrollableOfContainer );
7148                         this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
7150                         // Initial position after visible
7151                         this.position();
7152                 } else {
7153                         if ( this.$floatableWindow ) {
7154                                 this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
7155                                 this.$floatableWindow = null;
7156                         }
7158                         if ( this.$floatableClosestScrollable ) {
7159                                 this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
7160                                 this.$floatableClosestScrollable = null;
7161                         }
7163                         this.$floatable.css( { left: '', top: '' } );
7164                 }
7165         }
7167         return this;
7171  * Check whether the bottom edge of the given element is within the viewport of the given container.
7173  * @private
7174  * @param {jQuery} $element
7175  * @param {jQuery} $container
7176  * @return {boolean}
7177  */
7178 OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
7179         var elemRect, contRect,
7180                 leftEdgeInBounds = false,
7181                 bottomEdgeInBounds = false,
7182                 rightEdgeInBounds = false;
7184         elemRect = $element[ 0 ].getBoundingClientRect();
7185         if ( $container[ 0 ] === window ) {
7186                 contRect = {
7187                         top: 0,
7188                         left: 0,
7189                         right: document.documentElement.clientWidth,
7190                         bottom: document.documentElement.clientHeight
7191                 };
7192         } else {
7193                 contRect = $container[ 0 ].getBoundingClientRect();
7194         }
7196         // For completeness, if we still cared about topEdgeInBounds, that'd be:
7197         // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
7198         if ( elemRect.left >= contRect.left && elemRect.left <= contRect.right ) {
7199                 leftEdgeInBounds = true;
7200         }
7201         if ( elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom ) {
7202                 bottomEdgeInBounds = true;
7203         }
7204         if ( elemRect.right >= contRect.left && elemRect.right <= contRect.right ) {
7205                 rightEdgeInBounds = true;
7206         }
7208         // We only care that any part of the bottom edge is visible
7209         return bottomEdgeInBounds && ( leftEdgeInBounds || rightEdgeInBounds );
7213  * Position the floatable below its container.
7215  * This should only be done when both of them are attached to the DOM and visible.
7217  * @chainable
7218  */
7219 OO.ui.mixin.FloatableElement.prototype.position = function () {
7220         var pos;
7222         if ( !this.positioning ) {
7223                 return this;
7224         }
7226         if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
7227                 this.$floatable.addClass( 'oo-ui-element-hidden' );
7228                 return;
7229         } else {
7230                 this.$floatable.removeClass( 'oo-ui-element-hidden' );
7231         }
7233         if ( !this.needsCustomPosition ) {
7234                 return;
7235         }
7237         pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
7239         // Position under container
7240         pos.top += this.$floatableContainer.height();
7241         this.$floatable.css( pos );
7243         // We updated the position, so re-evaluate the clipping state.
7244         // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
7245         // will not notice the need to update itself.)
7246         // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
7247         // it not listen to the right events in the right places?
7248         if ( this.clip ) {
7249                 this.clip();
7250         }
7252         return this;
7256  * FloatingMenuSelectWidget is a menu that will stick under a specified
7257  * container, even when it is inserted elsewhere in the document (for example,
7258  * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
7259  * menu from being clipped too aggresively.
7261  * The menu's position is automatically calculated and maintained when the menu
7262  * is toggled or the window is resized.
7264  * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
7266  * @class
7267  * @extends OO.ui.MenuSelectWidget
7268  * @mixins OO.ui.mixin.FloatableElement
7270  * @constructor
7271  * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
7272  *   Deprecated, omit this parameter and specify `$container` instead.
7273  * @param {Object} [config] Configuration options
7274  * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
7275  */
7276 OO.ui.FloatingMenuSelectWidget = function OoUiFloatingMenuSelectWidget( inputWidget, config ) {
7277         // Allow 'inputWidget' parameter and config for backwards compatibility
7278         if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
7279                 config = inputWidget;
7280                 inputWidget = config.inputWidget;
7281         }
7283         // Configuration initialization
7284         config = config || {};
7286         // Parent constructor
7287         OO.ui.FloatingMenuSelectWidget.parent.call( this, config );
7289         // Properties (must be set before mixin constructors)
7290         this.inputWidget = inputWidget; // For backwards compatibility
7291         this.$container = config.$container || this.inputWidget.$element;
7293         // Mixins constructors
7294         OO.ui.mixin.FloatableElement.call( this, $.extend( {}, config, { $floatableContainer: this.$container } ) );
7296         // Initialization
7297         this.$element.addClass( 'oo-ui-floatingMenuSelectWidget' );
7298         // For backwards compatibility
7299         this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
7302 /* Setup */
7304 OO.inheritClass( OO.ui.FloatingMenuSelectWidget, OO.ui.MenuSelectWidget );
7305 OO.mixinClass( OO.ui.FloatingMenuSelectWidget, OO.ui.mixin.FloatableElement );
7307 // For backwards compatibility
7308 OO.ui.TextInputMenuSelectWidget = OO.ui.FloatingMenuSelectWidget;
7310 /* Methods */
7313  * @inheritdoc
7314  */
7315 OO.ui.FloatingMenuSelectWidget.prototype.toggle = function ( visible ) {
7316         var change;
7317         visible = visible === undefined ? !this.isVisible() : !!visible;
7318         change = visible !== this.isVisible();
7320         if ( change && visible ) {
7321                 // Make sure the width is set before the parent method runs.
7322                 this.setIdealSize( this.$container.width() );
7323         }
7325         // Parent method
7326         // This will call this.clip(), which is nonsensical since we're not positioned yet...
7327         OO.ui.FloatingMenuSelectWidget.parent.prototype.toggle.call( this, visible );
7329         if ( change ) {
7330                 this.togglePositioning( this.isVisible() );
7331         }
7333         return this;
7337  * Progress bars visually display the status of an operation, such as a download,
7338  * and can be either determinate or indeterminate:
7340  * - **determinate** process bars show the percent of an operation that is complete.
7342  * - **indeterminate** process bars use a visual display of motion to indicate that an operation
7343  *   is taking place. Because the extent of an indeterminate operation is unknown, the bar does
7344  *   not use percentages.
7346  * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
7348  *     @example
7349  *     // Examples of determinate and indeterminate progress bars.
7350  *     var progressBar1 = new OO.ui.ProgressBarWidget( {
7351  *         progress: 33
7352  *     } );
7353  *     var progressBar2 = new OO.ui.ProgressBarWidget();
7355  *     // Create a FieldsetLayout to layout progress bars
7356  *     var fieldset = new OO.ui.FieldsetLayout;
7357  *     fieldset.addItems( [
7358  *        new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
7359  *        new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
7360  *     ] );
7361  *     $( 'body' ).append( fieldset.$element );
7363  * @class
7364  * @extends OO.ui.Widget
7366  * @constructor
7367  * @param {Object} [config] Configuration options
7368  * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
7369  *  To create a determinate progress bar, specify a number that reflects the initial percent complete.
7370  *  By default, the progress bar is indeterminate.
7371  */
7372 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
7373         // Configuration initialization
7374         config = config || {};
7376         // Parent constructor
7377         OO.ui.ProgressBarWidget.parent.call( this, config );
7379         // Properties
7380         this.$bar = $( '<div>' );
7381         this.progress = null;
7383         // Initialization
7384         this.setProgress( config.progress !== undefined ? config.progress : false );
7385         this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
7386         this.$element
7387                 .attr( {
7388                         role: 'progressbar',
7389                         'aria-valuemin': 0,
7390                         'aria-valuemax': 100
7391                 } )
7392                 .addClass( 'oo-ui-progressBarWidget' )
7393                 .append( this.$bar );
7396 /* Setup */
7398 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
7400 /* Static Properties */
7402 OO.ui.ProgressBarWidget.static.tagName = 'div';
7404 /* Methods */
7407  * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
7409  * @return {number|boolean} Progress percent
7410  */
7411 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
7412         return this.progress;
7416  * Set the percent of the process completed or `false` for an indeterminate process.
7418  * @param {number|boolean} progress Progress percent or `false` for indeterminate
7419  */
7420 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
7421         this.progress = progress;
7423         if ( progress !== false ) {
7424                 this.$bar.css( 'width', this.progress + '%' );
7425                 this.$element.attr( 'aria-valuenow', this.progress );
7426         } else {
7427                 this.$bar.css( 'width', '' );
7428                 this.$element.removeAttr( 'aria-valuenow' );
7429         }
7430         this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress === false );
7434  * InputWidget is the base class for all input widgets, which
7435  * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
7436  * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
7437  * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7439  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7441  * @abstract
7442  * @class
7443  * @extends OO.ui.Widget
7444  * @mixins OO.ui.mixin.FlaggedElement
7445  * @mixins OO.ui.mixin.TabIndexedElement
7446  * @mixins OO.ui.mixin.TitledElement
7447  * @mixins OO.ui.mixin.AccessKeyedElement
7449  * @constructor
7450  * @param {Object} [config] Configuration options
7451  * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
7452  * @cfg {string} [value=''] The value of the input.
7453  * @cfg {string} [dir] The directionality of the input (ltr/rtl).
7454  * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
7455  *  before it is accepted.
7456  */
7457 OO.ui.InputWidget = function OoUiInputWidget( config ) {
7458         // Configuration initialization
7459         config = config || {};
7461         // Parent constructor
7462         OO.ui.InputWidget.parent.call( this, config );
7464         // Properties
7465         // See #reusePreInfuseDOM about config.$input
7466         this.$input = config.$input || this.getInputElement( config );
7467         this.value = '';
7468         this.inputFilter = config.inputFilter;
7470         // Mixin constructors
7471         OO.ui.mixin.FlaggedElement.call( this, config );
7472         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
7473         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
7474         OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$input } ) );
7476         // Events
7477         this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
7479         // Initialization
7480         this.$input
7481                 .addClass( 'oo-ui-inputWidget-input' )
7482                 .attr( 'name', config.name )
7483                 .prop( 'disabled', this.isDisabled() );
7484         this.$element
7485                 .addClass( 'oo-ui-inputWidget' )
7486                 .append( this.$input );
7487         this.setValue( config.value );
7488         if ( config.dir ) {
7489                 this.setDir( config.dir );
7490         }
7493 /* Setup */
7495 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
7496 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
7497 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
7498 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
7499 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
7501 /* Static Properties */
7503 OO.ui.InputWidget.static.supportsSimpleLabel = true;
7505 /* Static Methods */
7508  * @inheritdoc
7509  */
7510 OO.ui.InputWidget.static.reusePreInfuseDOM = function ( node, config ) {
7511         config = OO.ui.InputWidget.parent.static.reusePreInfuseDOM( node, config );
7512         // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
7513         config.$input = $( node ).find( '.oo-ui-inputWidget-input' );
7514         return config;
7518  * @inheritdoc
7519  */
7520 OO.ui.InputWidget.static.gatherPreInfuseState = function ( node, config ) {
7521         var state = OO.ui.InputWidget.parent.static.gatherPreInfuseState( node, config );
7522         if ( config.$input && config.$input.length ) {
7523                 state.value = config.$input.val();
7524                 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
7525                 state.focus = config.$input.is( ':focus' );
7526         }
7527         return state;
7530 /* Events */
7533  * @event change
7535  * A change event is emitted when the value of the input changes.
7537  * @param {string} value
7538  */
7540 /* Methods */
7543  * Get input element.
7545  * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
7546  * different circumstances. The element must have a `value` property (like form elements).
7548  * @protected
7549  * @param {Object} config Configuration options
7550  * @return {jQuery} Input element
7551  */
7552 OO.ui.InputWidget.prototype.getInputElement = function () {
7553         return $( '<input>' );
7557  * Handle potentially value-changing events.
7559  * @private
7560  * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
7561  */
7562 OO.ui.InputWidget.prototype.onEdit = function () {
7563         var widget = this;
7564         if ( !this.isDisabled() ) {
7565                 // Allow the stack to clear so the value will be updated
7566                 setTimeout( function () {
7567                         widget.setValue( widget.$input.val() );
7568                 } );
7569         }
7573  * Get the value of the input.
7575  * @return {string} Input value
7576  */
7577 OO.ui.InputWidget.prototype.getValue = function () {
7578         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7579         // it, and we won't know unless they're kind enough to trigger a 'change' event.
7580         var value = this.$input.val();
7581         if ( this.value !== value ) {
7582                 this.setValue( value );
7583         }
7584         return this.value;
7588  * Set the directionality of the input.
7590  * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
7591  * @chainable
7592  */
7593 OO.ui.InputWidget.prototype.setDir = function ( dir ) {
7594         this.$input.prop( 'dir', dir );
7595         return this;
7599  * Set the value of the input.
7601  * @param {string} value New value
7602  * @fires change
7603  * @chainable
7604  */
7605 OO.ui.InputWidget.prototype.setValue = function ( value ) {
7606         value = this.cleanUpValue( value );
7607         // Update the DOM if it has changed. Note that with cleanUpValue, it
7608         // is possible for the DOM value to change without this.value changing.
7609         if ( this.$input.val() !== value ) {
7610                 this.$input.val( value );
7611         }
7612         if ( this.value !== value ) {
7613                 this.value = value;
7614                 this.emit( 'change', this.value );
7615         }
7616         return this;
7620  * Clean up incoming value.
7622  * Ensures value is a string, and converts undefined and null to empty string.
7624  * @private
7625  * @param {string} value Original value
7626  * @return {string} Cleaned up value
7627  */
7628 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
7629         if ( value === undefined || value === null ) {
7630                 return '';
7631         } else if ( this.inputFilter ) {
7632                 return this.inputFilter( String( value ) );
7633         } else {
7634                 return String( value );
7635         }
7639  * Simulate the behavior of clicking on a label bound to this input. This method is only called by
7640  * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
7641  * called directly.
7642  */
7643 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
7644         if ( !this.isDisabled() ) {
7645                 if ( this.$input.is( ':checkbox, :radio' ) ) {
7646                         this.$input.click();
7647                 }
7648                 if ( this.$input.is( ':input' ) ) {
7649                         this.$input[ 0 ].focus();
7650                 }
7651         }
7655  * @inheritdoc
7656  */
7657 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
7658         OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
7659         if ( this.$input ) {
7660                 this.$input.prop( 'disabled', this.isDisabled() );
7661         }
7662         return this;
7666  * Focus the input.
7668  * @chainable
7669  */
7670 OO.ui.InputWidget.prototype.focus = function () {
7671         this.$input[ 0 ].focus();
7672         return this;
7676  * Blur the input.
7678  * @chainable
7679  */
7680 OO.ui.InputWidget.prototype.blur = function () {
7681         this.$input[ 0 ].blur();
7682         return this;
7686  * @inheritdoc
7687  */
7688 OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
7689         OO.ui.InputWidget.parent.prototype.restorePreInfuseState.call( this, state );
7690         if ( state.value !== undefined && state.value !== this.getValue() ) {
7691                 this.setValue( state.value );
7692         }
7693         if ( state.focus ) {
7694                 this.focus();
7695         }
7699  * ButtonInputWidget is used to submit HTML forms and is intended to be used within
7700  * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
7701  * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
7702  * HTML `<button>` (the default) or an HTML `<input>` tags. See the
7703  * [OOjs UI documentation on MediaWiki] [1] for more information.
7705  *     @example
7706  *     // A ButtonInputWidget rendered as an HTML button, the default.
7707  *     var button = new OO.ui.ButtonInputWidget( {
7708  *         label: 'Input button',
7709  *         icon: 'check',
7710  *         value: 'check'
7711  *     } );
7712  *     $( 'body' ).append( button.$element );
7714  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
7716  * @class
7717  * @extends OO.ui.InputWidget
7718  * @mixins OO.ui.mixin.ButtonElement
7719  * @mixins OO.ui.mixin.IconElement
7720  * @mixins OO.ui.mixin.IndicatorElement
7721  * @mixins OO.ui.mixin.LabelElement
7722  * @mixins OO.ui.mixin.TitledElement
7724  * @constructor
7725  * @param {Object} [config] Configuration options
7726  * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
7727  * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
7728  *  Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
7729  *  non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
7730  *  be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
7731  */
7732 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
7733         // Configuration initialization
7734         config = $.extend( { type: 'button', useInputTag: false }, config );
7736         // See InputWidget#reusePreInfuseDOM about config.$input
7737         if ( config.$input ) {
7738                 config.$input.empty();
7739         }
7741         // Properties (must be set before parent constructor, which calls #setValue)
7742         this.useInputTag = config.useInputTag;
7744         // Parent constructor
7745         OO.ui.ButtonInputWidget.parent.call( this, config );
7747         // Mixin constructors
7748         OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
7749         OO.ui.mixin.IconElement.call( this, config );
7750         OO.ui.mixin.IndicatorElement.call( this, config );
7751         OO.ui.mixin.LabelElement.call( this, config );
7752         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
7754         // Initialization
7755         if ( !config.useInputTag ) {
7756                 this.$input.append( this.$icon, this.$label, this.$indicator );
7757         }
7758         this.$element.addClass( 'oo-ui-buttonInputWidget' );
7761 /* Setup */
7763 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
7764 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
7765 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
7766 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
7767 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
7768 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
7770 /* Static Properties */
7773  * Disable generating `<label>` elements for buttons. One would very rarely need additional label
7774  * for a button, and it's already a big clickable target, and it causes unexpected rendering.
7775  */
7776 OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
7778 /* Methods */
7781  * @inheritdoc
7782  * @protected
7783  */
7784 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
7785         var type;
7786         type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ? config.type : 'button';
7787         return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
7791  * Set label value.
7793  * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
7795  * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
7796  *  text, or `null` for no label
7797  * @chainable
7798  */
7799 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
7800         if ( typeof label === 'function' ) {
7801                 label = OO.ui.resolveMsg( label );
7802         }
7804         if ( this.useInputTag ) {
7805                 // Discard non-plaintext labels
7806                 if ( typeof label !== 'string' ) {
7807                         label = '';
7808                 }
7810                 this.$input.val( label );
7811         }
7813         return OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
7817  * Set the value of the input.
7819  * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
7820  * they do not support {@link #value values}.
7822  * @param {string} value New value
7823  * @chainable
7824  */
7825 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
7826         if ( !this.useInputTag ) {
7827                 OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
7828         }
7829         return this;
7833  * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
7834  * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
7835  * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
7836  * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
7838  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7840  *     @example
7841  *     // An example of selected, unselected, and disabled checkbox inputs
7842  *     var checkbox1=new OO.ui.CheckboxInputWidget( {
7843  *          value: 'a',
7844  *          selected: true
7845  *     } );
7846  *     var checkbox2=new OO.ui.CheckboxInputWidget( {
7847  *         value: 'b'
7848  *     } );
7849  *     var checkbox3=new OO.ui.CheckboxInputWidget( {
7850  *         value:'c',
7851  *         disabled: true
7852  *     } );
7853  *     // Create a fieldset layout with fields for each checkbox.
7854  *     var fieldset = new OO.ui.FieldsetLayout( {
7855  *         label: 'Checkboxes'
7856  *     } );
7857  *     fieldset.addItems( [
7858  *         new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
7859  *         new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
7860  *         new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
7861  *     ] );
7862  *     $( 'body' ).append( fieldset.$element );
7864  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7866  * @class
7867  * @extends OO.ui.InputWidget
7869  * @constructor
7870  * @param {Object} [config] Configuration options
7871  * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
7872  */
7873 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
7874         // Configuration initialization
7875         config = config || {};
7877         // Parent constructor
7878         OO.ui.CheckboxInputWidget.parent.call( this, config );
7880         // Initialization
7881         this.$element
7882                 .addClass( 'oo-ui-checkboxInputWidget' )
7883                 // Required for pretty styling in MediaWiki theme
7884                 .append( $( '<span>' ) );
7885         this.setSelected( config.selected !== undefined ? config.selected : false );
7888 /* Setup */
7890 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
7892 /* Static Methods */
7895  * @inheritdoc
7896  */
7897 OO.ui.CheckboxInputWidget.static.gatherPreInfuseState = function ( node, config ) {
7898         var state = OO.ui.CheckboxInputWidget.parent.static.gatherPreInfuseState( node, config );
7899         state.checked = config.$input.prop( 'checked' );
7900         return state;
7903 /* Methods */
7906  * @inheritdoc
7907  * @protected
7908  */
7909 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
7910         return $( '<input>' ).attr( 'type', 'checkbox' );
7914  * @inheritdoc
7915  */
7916 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
7917         var widget = this;
7918         if ( !this.isDisabled() ) {
7919                 // Allow the stack to clear so the value will be updated
7920                 setTimeout( function () {
7921                         widget.setSelected( widget.$input.prop( 'checked' ) );
7922                 } );
7923         }
7927  * Set selection state of this checkbox.
7929  * @param {boolean} state `true` for selected
7930  * @chainable
7931  */
7932 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
7933         state = !!state;
7934         if ( this.selected !== state ) {
7935                 this.selected = state;
7936                 this.$input.prop( 'checked', this.selected );
7937                 this.emit( 'change', this.selected );
7938         }
7939         return this;
7943  * Check if this checkbox is selected.
7945  * @return {boolean} Checkbox is selected
7946  */
7947 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
7948         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7949         // it, and we won't know unless they're kind enough to trigger a 'change' event.
7950         var selected = this.$input.prop( 'checked' );
7951         if ( this.selected !== selected ) {
7952                 this.setSelected( selected );
7953         }
7954         return this.selected;
7958  * @inheritdoc
7959  */
7960 OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
7961         OO.ui.CheckboxInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
7962         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
7963                 this.setSelected( state.checked );
7964         }
7968  * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
7969  * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7970  * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7971  * more information about input widgets.
7973  * A DropdownInputWidget always has a value (one of the options is always selected), unless there
7974  * are no options. If no `value` configuration option is provided, the first option is selected.
7975  * If you need a state representing no value (no option being selected), use a DropdownWidget.
7977  * This and OO.ui.RadioSelectInputWidget support the same configuration options.
7979  *     @example
7980  *     // Example: A DropdownInputWidget with three options
7981  *     var dropdownInput = new OO.ui.DropdownInputWidget( {
7982  *         options: [
7983  *             { data: 'a', label: 'First' },
7984  *             { data: 'b', label: 'Second'},
7985  *             { data: 'c', label: 'Third' }
7986  *         ]
7987  *     } );
7988  *     $( 'body' ).append( dropdownInput.$element );
7990  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7992  * @class
7993  * @extends OO.ui.InputWidget
7994  * @mixins OO.ui.mixin.TitledElement
7996  * @constructor
7997  * @param {Object} [config] Configuration options
7998  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7999  * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
8000  */
8001 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
8002         // Configuration initialization
8003         config = config || {};
8005         // See InputWidget#reusePreInfuseDOM about config.$input
8006         if ( config.$input ) {
8007                 config.$input.addClass( 'oo-ui-element-hidden' );
8008         }
8010         // Properties (must be done before parent constructor which calls #setDisabled)
8011         this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
8013         // Parent constructor
8014         OO.ui.DropdownInputWidget.parent.call( this, config );
8016         // Mixin constructors
8017         OO.ui.mixin.TitledElement.call( this, config );
8019         // Events
8020         this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
8022         // Initialization
8023         this.setOptions( config.options || [] );
8024         this.$element
8025                 .addClass( 'oo-ui-dropdownInputWidget' )
8026                 .append( this.dropdownWidget.$element );
8029 /* Setup */
8031 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
8032 OO.mixinClass( OO.ui.DropdownInputWidget, OO.ui.mixin.TitledElement );
8034 /* Methods */
8037  * @inheritdoc
8038  * @protected
8039  */
8040 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
8041         return $( '<input>' ).attr( 'type', 'hidden' );
8045  * Handles menu select events.
8047  * @private
8048  * @param {OO.ui.MenuOptionWidget} item Selected menu item
8049  */
8050 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
8051         this.setValue( item.getData() );
8055  * @inheritdoc
8056  */
8057 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
8058         value = this.cleanUpValue( value );
8059         this.dropdownWidget.getMenu().selectItemByData( value );
8060         OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
8061         return this;
8065  * @inheritdoc
8066  */
8067 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
8068         this.dropdownWidget.setDisabled( state );
8069         OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
8070         return this;
8074  * Set the options available for this input.
8076  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8077  * @chainable
8078  */
8079 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
8080         var
8081                 value = this.getValue(),
8082                 widget = this;
8084         // Rebuild the dropdown menu
8085         this.dropdownWidget.getMenu()
8086                 .clearItems()
8087                 .addItems( options.map( function ( opt ) {
8088                         var optValue = widget.cleanUpValue( opt.data );
8089                         return new OO.ui.MenuOptionWidget( {
8090                                 data: optValue,
8091                                 label: opt.label !== undefined ? opt.label : optValue
8092                         } );
8093                 } ) );
8095         // Restore the previous value, or reset to something sensible
8096         if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
8097                 // Previous value is still available, ensure consistency with the dropdown
8098                 this.setValue( value );
8099         } else {
8100                 // No longer valid, reset
8101                 if ( options.length ) {
8102                         this.setValue( options[ 0 ].data );
8103                 }
8104         }
8106         return this;
8110  * @inheritdoc
8111  */
8112 OO.ui.DropdownInputWidget.prototype.focus = function () {
8113         this.dropdownWidget.getMenu().toggle( true );
8114         return this;
8118  * @inheritdoc
8119  */
8120 OO.ui.DropdownInputWidget.prototype.blur = function () {
8121         this.dropdownWidget.getMenu().toggle( false );
8122         return this;
8126  * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
8127  * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
8128  * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
8129  * please see the [OOjs UI documentation on MediaWiki][1].
8131  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8133  *     @example
8134  *     // An example of selected, unselected, and disabled radio inputs
8135  *     var radio1 = new OO.ui.RadioInputWidget( {
8136  *         value: 'a',
8137  *         selected: true
8138  *     } );
8139  *     var radio2 = new OO.ui.RadioInputWidget( {
8140  *         value: 'b'
8141  *     } );
8142  *     var radio3 = new OO.ui.RadioInputWidget( {
8143  *         value: 'c',
8144  *         disabled: true
8145  *     } );
8146  *     // Create a fieldset layout with fields for each radio button.
8147  *     var fieldset = new OO.ui.FieldsetLayout( {
8148  *         label: 'Radio inputs'
8149  *     } );
8150  *     fieldset.addItems( [
8151  *         new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
8152  *         new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
8153  *         new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
8154  *     ] );
8155  *     $( 'body' ).append( fieldset.$element );
8157  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8159  * @class
8160  * @extends OO.ui.InputWidget
8162  * @constructor
8163  * @param {Object} [config] Configuration options
8164  * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
8165  */
8166 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
8167         // Configuration initialization
8168         config = config || {};
8170         // Parent constructor
8171         OO.ui.RadioInputWidget.parent.call( this, config );
8173         // Initialization
8174         this.$element
8175                 .addClass( 'oo-ui-radioInputWidget' )
8176                 // Required for pretty styling in MediaWiki theme
8177                 .append( $( '<span>' ) );
8178         this.setSelected( config.selected !== undefined ? config.selected : false );
8181 /* Setup */
8183 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
8185 /* Static Methods */
8188  * @inheritdoc
8189  */
8190 OO.ui.RadioInputWidget.static.gatherPreInfuseState = function ( node, config ) {
8191         var state = OO.ui.RadioInputWidget.parent.static.gatherPreInfuseState( node, config );
8192         state.checked = config.$input.prop( 'checked' );
8193         return state;
8196 /* Methods */
8199  * @inheritdoc
8200  * @protected
8201  */
8202 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
8203         return $( '<input>' ).attr( 'type', 'radio' );
8207  * @inheritdoc
8208  */
8209 OO.ui.RadioInputWidget.prototype.onEdit = function () {
8210         // RadioInputWidget doesn't track its state.
8214  * Set selection state of this radio button.
8216  * @param {boolean} state `true` for selected
8217  * @chainable
8218  */
8219 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
8220         // RadioInputWidget doesn't track its state.
8221         this.$input.prop( 'checked', state );
8222         return this;
8226  * Check if this radio button is selected.
8228  * @return {boolean} Radio is selected
8229  */
8230 OO.ui.RadioInputWidget.prototype.isSelected = function () {
8231         return this.$input.prop( 'checked' );
8235  * @inheritdoc
8236  */
8237 OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
8238         OO.ui.RadioInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
8239         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
8240                 this.setSelected( state.checked );
8241         }
8245  * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
8246  * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
8247  * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
8248  * more information about input widgets.
8250  * This and OO.ui.DropdownInputWidget support the same configuration options.
8252  *     @example
8253  *     // Example: A RadioSelectInputWidget with three options
8254  *     var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
8255  *         options: [
8256  *             { data: 'a', label: 'First' },
8257  *             { data: 'b', label: 'Second'},
8258  *             { data: 'c', label: 'Third' }
8259  *         ]
8260  *     } );
8261  *     $( 'body' ).append( radioSelectInput.$element );
8263  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8265  * @class
8266  * @extends OO.ui.InputWidget
8268  * @constructor
8269  * @param {Object} [config] Configuration options
8270  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8271  */
8272 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
8273         // Configuration initialization
8274         config = config || {};
8276         // Properties (must be done before parent constructor which calls #setDisabled)
8277         this.radioSelectWidget = new OO.ui.RadioSelectWidget();
8279         // Parent constructor
8280         OO.ui.RadioSelectInputWidget.parent.call( this, config );
8282         // Events
8283         this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
8285         // Initialization
8286         this.setOptions( config.options || [] );
8287         this.$element
8288                 .addClass( 'oo-ui-radioSelectInputWidget' )
8289                 .append( this.radioSelectWidget.$element );
8292 /* Setup */
8294 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
8296 /* Static Properties */
8298 OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
8300 /* Static Methods */
8303  * @inheritdoc
8304  */
8305 OO.ui.RadioSelectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
8306         var state = OO.ui.RadioSelectInputWidget.parent.static.gatherPreInfuseState( node, config );
8307         state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
8308         return state;
8312  * @inheritdoc
8313  */
8314 OO.ui.RadioSelectInputWidget.static.reusePreInfuseDOM = function ( node, config ) {
8315         config = OO.ui.RadioSelectInputWidget.parent.static.reusePreInfuseDOM( node, config );
8316         // Cannot reuse the `<input type=radio>` set
8317         delete config.$input;
8318         return config;
8321 /* Methods */
8324  * @inheritdoc
8325  * @protected
8326  */
8327 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
8328         return $( '<input>' ).attr( 'type', 'hidden' );
8332  * Handles menu select events.
8334  * @private
8335  * @param {OO.ui.RadioOptionWidget} item Selected menu item
8336  */
8337 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
8338         this.setValue( item.getData() );
8342  * @inheritdoc
8343  */
8344 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
8345         value = this.cleanUpValue( value );
8346         this.radioSelectWidget.selectItemByData( value );
8347         OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
8348         return this;
8352  * @inheritdoc
8353  */
8354 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
8355         this.radioSelectWidget.setDisabled( state );
8356         OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
8357         return this;
8361  * Set the options available for this input.
8363  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8364  * @chainable
8365  */
8366 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
8367         var
8368                 value = this.getValue(),
8369                 widget = this;
8371         // Rebuild the radioSelect menu
8372         this.radioSelectWidget
8373                 .clearItems()
8374                 .addItems( options.map( function ( opt ) {
8375                         var optValue = widget.cleanUpValue( opt.data );
8376                         return new OO.ui.RadioOptionWidget( {
8377                                 data: optValue,
8378                                 label: opt.label !== undefined ? opt.label : optValue
8379                         } );
8380                 } ) );
8382         // Restore the previous value, or reset to something sensible
8383         if ( this.radioSelectWidget.getItemFromData( value ) ) {
8384                 // Previous value is still available, ensure consistency with the radioSelect
8385                 this.setValue( value );
8386         } else {
8387                 // No longer valid, reset
8388                 if ( options.length ) {
8389                         this.setValue( options[ 0 ].data );
8390                 }
8391         }
8393         return this;
8397  * CheckboxMultiselectInputWidget is a
8398  * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
8399  * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
8400  * HTML `<input type=checkbox>` tags. Please see the [OOjs UI documentation on MediaWiki][1] for
8401  * more information about input widgets.
8403  *     @example
8404  *     // Example: A CheckboxMultiselectInputWidget with three options
8405  *     var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
8406  *         options: [
8407  *             { data: 'a', label: 'First' },
8408  *             { data: 'b', label: 'Second'},
8409  *             { data: 'c', label: 'Third' }
8410  *         ]
8411  *     } );
8412  *     $( 'body' ).append( multiselectInput.$element );
8414  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8416  * @class
8417  * @extends OO.ui.InputWidget
8419  * @constructor
8420  * @param {Object} [config] Configuration options
8421  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8422  */
8423 OO.ui.CheckboxMultiselectInputWidget = function OoUiCheckboxMultiselectInputWidget( config ) {
8424         // Configuration initialization
8425         config = config || {};
8427         // Properties (must be done before parent constructor which calls #setDisabled)
8428         this.checkboxMultiselectWidget = new OO.ui.CheckboxMultiselectWidget();
8430         // Parent constructor
8431         OO.ui.CheckboxMultiselectInputWidget.parent.call( this, config );
8433         // Properties
8434         this.inputName = config.name;
8436         // Initialization
8437         this.$element
8438                 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
8439                 .append( this.checkboxMultiselectWidget.$element );
8440         // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
8441         this.$input.detach();
8442         this.setOptions( config.options || [] );
8443         // Have to repeat this from parent, as we need options to be set up for this to make sense
8444         this.setValue( config.value );
8447 /* Setup */
8449 OO.inheritClass( OO.ui.CheckboxMultiselectInputWidget, OO.ui.InputWidget );
8451 /* Static Properties */
8453 OO.ui.CheckboxMultiselectInputWidget.static.supportsSimpleLabel = false;
8455 /* Static Methods */
8458  * @inheritdoc
8459  */
8460 OO.ui.CheckboxMultiselectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
8461         var state = OO.ui.CheckboxMultiselectInputWidget.parent.static.gatherPreInfuseState( node, config );
8462         state.value = $( node ).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8463                 .toArray().map( function ( el ) { return el.value; } );
8464         return state;
8468  * @inheritdoc
8469  */
8470 OO.ui.CheckboxMultiselectInputWidget.static.reusePreInfuseDOM = function ( node, config ) {
8471         config = OO.ui.CheckboxMultiselectInputWidget.parent.static.reusePreInfuseDOM( node, config );
8472         // Cannot reuse the `<input type=checkbox>` set
8473         delete config.$input;
8474         return config;
8477 /* Methods */
8480  * @inheritdoc
8481  * @protected
8482  */
8483 OO.ui.CheckboxMultiselectInputWidget.prototype.getInputElement = function () {
8484         // Actually unused
8485         return $( '<div>' );
8489  * @inheritdoc
8490  */
8491 OO.ui.CheckboxMultiselectInputWidget.prototype.getValue = function () {
8492         var value = this.$element.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
8493                 .toArray().map( function ( el ) { return el.value; } );
8494         if ( this.value !== value ) {
8495                 this.setValue( value );
8496         }
8497         return this.value;
8501  * @inheritdoc
8502  */
8503 OO.ui.CheckboxMultiselectInputWidget.prototype.setValue = function ( value ) {
8504         value = this.cleanUpValue( value );
8505         this.checkboxMultiselectWidget.selectItemsByData( value );
8506         OO.ui.CheckboxMultiselectInputWidget.parent.prototype.setValue.call( this, value );
8507         return this;
8511  * Clean up incoming value.
8513  * @param {string[]} value Original value
8514  * @return {string[]} Cleaned up value
8515  */
8516 OO.ui.CheckboxMultiselectInputWidget.prototype.cleanUpValue = function ( value ) {
8517         var i, singleValue,
8518                 cleanValue = [];
8519         if ( !Array.isArray( value ) ) {
8520                 return cleanValue;
8521         }
8522         for ( i = 0; i < value.length; i++ ) {
8523                 singleValue =
8524                         OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call( this, value[ i ] );
8525                 // Remove options that we don't have here
8526                 if ( !this.checkboxMultiselectWidget.getItemFromData( singleValue ) ) {
8527                         continue;
8528                 }
8529                 cleanValue.push( singleValue );
8530         }
8531         return cleanValue;
8535  * @inheritdoc
8536  */
8537 OO.ui.CheckboxMultiselectInputWidget.prototype.setDisabled = function ( state ) {
8538         this.checkboxMultiselectWidget.setDisabled( state );
8539         OO.ui.CheckboxMultiselectInputWidget.parent.prototype.setDisabled.call( this, state );
8540         return this;
8544  * Set the options available for this input.
8546  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8547  * @chainable
8548  */
8549 OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options ) {
8550         var widget = this;
8552         // Rebuild the checkboxMultiselectWidget menu
8553         this.checkboxMultiselectWidget
8554                 .clearItems()
8555                 .addItems( options.map( function ( opt ) {
8556                         var optValue, item;
8557                         optValue =
8558                                 OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call( widget, opt.data );
8559                         item = new OO.ui.CheckboxMultioptionWidget( {
8560                                 data: optValue,
8561                                 label: opt.label !== undefined ? opt.label : optValue
8562                         } );
8563                         // Set the 'name' and 'value' for form submission
8564                         item.checkbox.$input.attr( 'name', widget.inputName );
8565                         item.checkbox.setValue( optValue );
8566                         return item;
8567                 } ) );
8569         // Re-set the value, checking the checkboxes as needed.
8570         // This will also get rid of any stale options that we just removed.
8571         this.setValue( this.getValue() );
8573         return this;
8577  * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
8578  * size of the field as well as its presentation. In addition, these widgets can be configured
8579  * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
8580  * validation-pattern (used to determine if an input value is valid or not) and an input filter,
8581  * which modifies incoming values rather than validating them.
8582  * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8584  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8586  *     @example
8587  *     // Example of a text input widget
8588  *     var textInput = new OO.ui.TextInputWidget( {
8589  *         value: 'Text input'
8590  *     } )
8591  *     $( 'body' ).append( textInput.$element );
8593  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8595  * @class
8596  * @extends OO.ui.InputWidget
8597  * @mixins OO.ui.mixin.IconElement
8598  * @mixins OO.ui.mixin.IndicatorElement
8599  * @mixins OO.ui.mixin.PendingElement
8600  * @mixins OO.ui.mixin.LabelElement
8602  * @constructor
8603  * @param {Object} [config] Configuration options
8604  * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
8605  *  'email', 'url', 'date', 'month' or 'number'. Ignored if `multiline` is true.
8607  *  Some values of `type` result in additional behaviors:
8609  *  - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
8610  *    empties the text field
8611  * @cfg {string} [placeholder] Placeholder text
8612  * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
8613  *  instruct the browser to focus this widget.
8614  * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
8615  * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
8616  * @cfg {boolean} [multiline=false] Allow multiple lines of text
8617  * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
8618  *  specifies minimum number of rows to display.
8619  * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
8620  *  Use the #maxRows config to specify a maximum number of displayed rows.
8621  * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
8622  *  Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
8623  * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
8624  *  the value or placeholder text: `'before'` or `'after'`
8625  * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
8626  * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
8627  * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
8628  *  pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
8629  *  (the value must contain only numbers); when RegExp, a regular expression that must match the
8630  *  value for it to be considered valid; when Function, a function receiving the value as parameter
8631  *  that must return true, or promise resolving to true, for it to be considered valid.
8632  */
8633 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
8634         // Configuration initialization
8635         config = $.extend( {
8636                 type: 'text',
8637                 labelPosition: 'after'
8638         }, config );
8640         if ( config.type === 'search' ) {
8641                 OO.ui.warnDeprecation( 'TextInputWidget: config.type=\'search\' is deprecated. Use the SearchInputWidget instead. See T148471 for details.' );
8642                 if ( config.icon === undefined ) {
8643                         config.icon = 'search';
8644                 }
8645                 // indicator: 'clear' is set dynamically later, depending on value
8646         }
8648         // Parent constructor
8649         OO.ui.TextInputWidget.parent.call( this, config );
8651         // Mixin constructors
8652         OO.ui.mixin.IconElement.call( this, config );
8653         OO.ui.mixin.IndicatorElement.call( this, config );
8654         OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
8655         OO.ui.mixin.LabelElement.call( this, config );
8657         // Properties
8658         this.type = this.getSaneType( config );
8659         this.readOnly = false;
8660         this.required = false;
8661         this.multiline = !!config.multiline;
8662         this.autosize = !!config.autosize;
8663         this.minRows = config.rows !== undefined ? config.rows : '';
8664         this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
8665         this.validate = null;
8666         this.styleHeight = null;
8667         this.scrollWidth = null;
8669         // Clone for resizing
8670         if ( this.autosize ) {
8671                 this.$clone = this.$input
8672                         .clone()
8673                         .insertAfter( this.$input )
8674                         .attr( 'aria-hidden', 'true' )
8675                         .addClass( 'oo-ui-element-hidden' );
8676         }
8678         this.setValidation( config.validate );
8679         this.setLabelPosition( config.labelPosition );
8681         // Events
8682         this.$input.on( {
8683                 keypress: this.onKeyPress.bind( this ),
8684                 blur: this.onBlur.bind( this ),
8685                 focus: this.onFocus.bind( this )
8686         } );
8687         this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
8688         this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
8689         this.on( 'labelChange', this.updatePosition.bind( this ) );
8690         this.connect( this, {
8691                 change: 'onChange',
8692                 disable: 'onDisable'
8693         } );
8694         this.on( 'change', OO.ui.debounce( this.onDebouncedChange.bind( this ), 250 ) );
8696         // Initialization
8697         this.$element
8698                 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
8699                 .append( this.$icon, this.$indicator );
8700         this.setReadOnly( !!config.readOnly );
8701         this.setRequired( !!config.required );
8702         this.updateSearchIndicator();
8703         if ( config.placeholder !== undefined ) {
8704                 this.$input.attr( 'placeholder', config.placeholder );
8705         }
8706         if ( config.maxLength !== undefined ) {
8707                 this.$input.attr( 'maxlength', config.maxLength );
8708         }
8709         if ( config.autofocus ) {
8710                 this.$input.attr( 'autofocus', 'autofocus' );
8711         }
8712         if ( config.autocomplete === false ) {
8713                 this.$input.attr( 'autocomplete', 'off' );
8714                 // Turning off autocompletion also disables "form caching" when the user navigates to a
8715                 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
8716                 $( window ).on( {
8717                         beforeunload: function () {
8718                                 this.$input.removeAttr( 'autocomplete' );
8719                         }.bind( this ),
8720                         pageshow: function () {
8721                                 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
8722                                 // whole page... it shouldn't hurt, though.
8723                                 this.$input.attr( 'autocomplete', 'off' );
8724                         }.bind( this )
8725                 } );
8726         }
8727         if ( this.multiline && config.rows ) {
8728                 this.$input.attr( 'rows', config.rows );
8729         }
8730         if ( this.label || config.autosize ) {
8731                 this.isWaitingToBeAttached = true;
8732                 this.installParentChangeDetector();
8733         }
8736 /* Setup */
8738 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
8739 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
8740 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
8741 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
8742 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
8744 /* Static Properties */
8746 OO.ui.TextInputWidget.static.validationPatterns = {
8747         'non-empty': /.+/,
8748         integer: /^\d+$/
8751 /* Static Methods */
8754  * @inheritdoc
8755  */
8756 OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
8757         var state = OO.ui.TextInputWidget.parent.static.gatherPreInfuseState( node, config );
8758         if ( config.multiline ) {
8759                 state.scrollTop = config.$input.scrollTop();
8760         }
8761         return state;
8764 /* Events */
8767  * An `enter` event is emitted when the user presses 'enter' inside the text box.
8769  * Not emitted if the input is multiline.
8771  * @event enter
8772  */
8775  * A `resize` event is emitted when autosize is set and the widget resizes
8777  * @event resize
8778  */
8780 /* Methods */
8783  * Handle icon mouse down events.
8785  * @private
8786  * @param {jQuery.Event} e Mouse down event
8787  */
8788 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
8789         if ( e.which === OO.ui.MouseButtons.LEFT ) {
8790                 this.$input[ 0 ].focus();
8791                 return false;
8792         }
8796  * Handle indicator mouse down events.
8798  * @private
8799  * @param {jQuery.Event} e Mouse down event
8800  */
8801 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
8802         if ( e.which === OO.ui.MouseButtons.LEFT ) {
8803                 if ( this.type === 'search' ) {
8804                         // Clear the text field
8805                         this.setValue( '' );
8806                 }
8807                 this.$input[ 0 ].focus();
8808                 return false;
8809         }
8813  * Handle key press events.
8815  * @private
8816  * @param {jQuery.Event} e Key press event
8817  * @fires enter If enter key is pressed and input is not multiline
8818  */
8819 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
8820         if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
8821                 this.emit( 'enter', e );
8822         }
8826  * Handle blur events.
8828  * @private
8829  * @param {jQuery.Event} e Blur event
8830  */
8831 OO.ui.TextInputWidget.prototype.onBlur = function () {
8832         this.setValidityFlag();
8836  * Handle focus events.
8838  * @private
8839  * @param {jQuery.Event} e Focus event
8840  */
8841 OO.ui.TextInputWidget.prototype.onFocus = function () {
8842         if ( this.isWaitingToBeAttached ) {
8843                 // If we've received focus, then we must be attached to the document, and if
8844                 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
8845                 this.onElementAttach();
8846         }
8847         this.setValidityFlag( true );
8851  * Handle element attach events.
8853  * @private
8854  * @param {jQuery.Event} e Element attach event
8855  */
8856 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
8857         this.isWaitingToBeAttached = false;
8858         // Any previously calculated size is now probably invalid if we reattached elsewhere
8859         this.valCache = null;
8860         this.adjustSize();
8861         this.positionLabel();
8865  * Handle change events.
8867  * @param {string} value
8868  * @private
8869  */
8870 OO.ui.TextInputWidget.prototype.onChange = function () {
8871         this.updateSearchIndicator();
8872         this.adjustSize();
8876  * Handle debounced change events.
8878  * @param {string} value
8879  * @private
8880  */
8881 OO.ui.TextInputWidget.prototype.onDebouncedChange = function () {
8882         this.setValidityFlag();
8886  * Handle disable events.
8888  * @param {boolean} disabled Element is disabled
8889  * @private
8890  */
8891 OO.ui.TextInputWidget.prototype.onDisable = function () {
8892         this.updateSearchIndicator();
8896  * Check if the input is {@link #readOnly read-only}.
8898  * @return {boolean}
8899  */
8900 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
8901         return this.readOnly;
8905  * Set the {@link #readOnly read-only} state of the input.
8907  * @param {boolean} state Make input read-only
8908  * @chainable
8909  */
8910 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
8911         this.readOnly = !!state;
8912         this.$input.prop( 'readOnly', this.readOnly );
8913         this.updateSearchIndicator();
8914         return this;
8918  * Check if the input is {@link #required required}.
8920  * @return {boolean}
8921  */
8922 OO.ui.TextInputWidget.prototype.isRequired = function () {
8923         return this.required;
8927  * Set the {@link #required required} state of the input.
8929  * @param {boolean} state Make input required
8930  * @chainable
8931  */
8932 OO.ui.TextInputWidget.prototype.setRequired = function ( state ) {
8933         this.required = !!state;
8934         if ( this.required ) {
8935                 this.$input
8936                         .attr( 'required', 'required' )
8937                         .attr( 'aria-required', 'true' );
8938                 if ( this.getIndicator() === null ) {
8939                         this.setIndicator( 'required' );
8940                 }
8941         } else {
8942                 this.$input
8943                         .removeAttr( 'required' )
8944                         .removeAttr( 'aria-required' );
8945                 if ( this.getIndicator() === 'required' ) {
8946                         this.setIndicator( null );
8947                 }
8948         }
8949         this.updateSearchIndicator();
8950         return this;
8954  * Support function for making #onElementAttach work across browsers.
8956  * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
8957  * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
8959  * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
8960  * first time that the element gets attached to the documented.
8961  */
8962 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
8963         var mutationObserver, onRemove, topmostNode, fakeParentNode,
8964                 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
8965                 widget = this;
8967         if ( MutationObserver ) {
8968                 // The new way. If only it wasn't so ugly.
8970                 if ( this.isElementAttached() ) {
8971                         // Widget is attached already, do nothing. This breaks the functionality of this function when
8972                         // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
8973                         // would require observation of the whole document, which would hurt performance of other,
8974                         // more important code.
8975                         return;
8976                 }
8978                 // Find topmost node in the tree
8979                 topmostNode = this.$element[ 0 ];
8980                 while ( topmostNode.parentNode ) {
8981                         topmostNode = topmostNode.parentNode;
8982                 }
8984                 // We have no way to detect the $element being attached somewhere without observing the entire
8985                 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
8986                 // parent node of $element, and instead detect when $element is removed from it (and thus
8987                 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
8988                 // doesn't get attached, we end up back here and create the parent.
8990                 mutationObserver = new MutationObserver( function ( mutations ) {
8991                         var i, j, removedNodes;
8992                         for ( i = 0; i < mutations.length; i++ ) {
8993                                 removedNodes = mutations[ i ].removedNodes;
8994                                 for ( j = 0; j < removedNodes.length; j++ ) {
8995                                         if ( removedNodes[ j ] === topmostNode ) {
8996                                                 setTimeout( onRemove, 0 );
8997                                                 return;
8998                                         }
8999                                 }
9000                         }
9001                 } );
9003                 onRemove = function () {
9004                         // If the node was attached somewhere else, report it
9005                         if ( widget.isElementAttached() ) {
9006                                 widget.onElementAttach();
9007                         }
9008                         mutationObserver.disconnect();
9009                         widget.installParentChangeDetector();
9010                 };
9012                 // Create a fake parent and observe it
9013                 fakeParentNode = $( '<div>' ).append( topmostNode )[ 0 ];
9014                 mutationObserver.observe( fakeParentNode, { childList: true } );
9015         } else {
9016                 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
9017                 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
9018                 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
9019         }
9023  * Automatically adjust the size of the text input.
9025  * This only affects #multiline inputs that are {@link #autosize autosized}.
9027  * @chainable
9028  * @fires resize
9029  */
9030 OO.ui.TextInputWidget.prototype.adjustSize = function () {
9031         var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError,
9032                 idealHeight, newHeight, scrollWidth, property;
9034         if ( this.isWaitingToBeAttached ) {
9035                 // #onElementAttach will be called soon, which calls this method
9036                 return this;
9037         }
9039         if ( this.multiline && this.$input.val() !== this.valCache ) {
9040                 if ( this.autosize ) {
9041                         this.$clone
9042                                 .val( this.$input.val() )
9043                                 .attr( 'rows', this.minRows )
9044                                 // Set inline height property to 0 to measure scroll height
9045                                 .css( 'height', 0 );
9047                         this.$clone.removeClass( 'oo-ui-element-hidden' );
9049                         this.valCache = this.$input.val();
9051                         scrollHeight = this.$clone[ 0 ].scrollHeight;
9053                         // Remove inline height property to measure natural heights
9054                         this.$clone.css( 'height', '' );
9055                         innerHeight = this.$clone.innerHeight();
9056                         outerHeight = this.$clone.outerHeight();
9058                         // Measure max rows height
9059                         this.$clone
9060                                 .attr( 'rows', this.maxRows )
9061                                 .css( 'height', 'auto' )
9062                                 .val( '' );
9063                         maxInnerHeight = this.$clone.innerHeight();
9065                         // Difference between reported innerHeight and scrollHeight with no scrollbars present.
9066                         // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
9067                         measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
9068                         idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
9070                         this.$clone.addClass( 'oo-ui-element-hidden' );
9072                         // Only apply inline height when expansion beyond natural height is needed
9073                         // Use the difference between the inner and outer height as a buffer
9074                         newHeight = idealHeight > innerHeight ? idealHeight + ( outerHeight - innerHeight ) : '';
9075                         if ( newHeight !== this.styleHeight ) {
9076                                 this.$input.css( 'height', newHeight );
9077                                 this.styleHeight = newHeight;
9078                                 this.emit( 'resize' );
9079                         }
9080                 }
9081                 scrollWidth = this.$input[ 0 ].offsetWidth - this.$input[ 0 ].clientWidth;
9082                 if ( scrollWidth !== this.scrollWidth ) {
9083                         property = this.$element.css( 'direction' ) === 'rtl' ? 'left' : 'right';
9084                         // Reset
9085                         this.$label.css( { right: '', left: '' } );
9086                         this.$indicator.css( { right: '', left: '' } );
9088                         if ( scrollWidth ) {
9089                                 this.$indicator.css( property, scrollWidth );
9090                                 if ( this.labelPosition === 'after' ) {
9091                                         this.$label.css( property, scrollWidth );
9092                                 }
9093                         }
9095                         this.scrollWidth = scrollWidth;
9096                         this.positionLabel();
9097                 }
9098         }
9099         return this;
9103  * @inheritdoc
9104  * @protected
9105  */
9106 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
9107         if ( config.multiline ) {
9108                 return $( '<textarea>' );
9109         } else if ( this.getSaneType( config ) === 'number' ) {
9110                 return $( '<input>' )
9111                         .attr( 'step', 'any' )
9112                         .attr( 'type', 'number' );
9113         } else {
9114                 return $( '<input>' ).attr( 'type', this.getSaneType( config ) );
9115         }
9119  * Get sanitized value for 'type' for given config.
9121  * @param {Object} config Configuration options
9122  * @return {string|null}
9123  * @private
9124  */
9125 OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
9126         var allowedTypes = [
9127                 'text',
9128                 'password',
9129                 'search',
9130                 'email',
9131                 'url',
9132                 'date',
9133                 'month',
9134                 'number'
9135         ];
9136         return allowedTypes.indexOf( config.type ) !== -1 ? config.type : 'text';
9140  * Check if the input supports multiple lines.
9142  * @return {boolean}
9143  */
9144 OO.ui.TextInputWidget.prototype.isMultiline = function () {
9145         return !!this.multiline;
9149  * Check if the input automatically adjusts its size.
9151  * @return {boolean}
9152  */
9153 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
9154         return !!this.autosize;
9158  * Focus the input and select a specified range within the text.
9160  * @param {number} from Select from offset
9161  * @param {number} [to] Select to offset, defaults to from
9162  * @chainable
9163  */
9164 OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
9165         var isBackwards, start, end,
9166                 input = this.$input[ 0 ];
9168         to = to || from;
9170         isBackwards = to < from;
9171         start = isBackwards ? to : from;
9172         end = isBackwards ? from : to;
9174         this.focus();
9176         try {
9177                 input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
9178         } catch ( e ) {
9179                 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
9180                 // Rather than expensively check if the input is attached every time, just check
9181                 // if it was the cause of an error being thrown. If not, rethrow the error.
9182                 if ( this.getElementDocument().body.contains( input ) ) {
9183                         throw e;
9184                 }
9185         }
9186         return this;
9190  * Get an object describing the current selection range in a directional manner
9192  * @return {Object} Object containing 'from' and 'to' offsets
9193  */
9194 OO.ui.TextInputWidget.prototype.getRange = function () {
9195         var input = this.$input[ 0 ],
9196                 start = input.selectionStart,
9197                 end = input.selectionEnd,
9198                 isBackwards = input.selectionDirection === 'backward';
9200         return {
9201                 from: isBackwards ? end : start,
9202                 to: isBackwards ? start : end
9203         };
9207  * Get the length of the text input value.
9209  * This could differ from the length of #getValue if the
9210  * value gets filtered
9212  * @return {number} Input length
9213  */
9214 OO.ui.TextInputWidget.prototype.getInputLength = function () {
9215         return this.$input[ 0 ].value.length;
9219  * Focus the input and select the entire text.
9221  * @chainable
9222  */
9223 OO.ui.TextInputWidget.prototype.select = function () {
9224         return this.selectRange( 0, this.getInputLength() );
9228  * Focus the input and move the cursor to the start.
9230  * @chainable
9231  */
9232 OO.ui.TextInputWidget.prototype.moveCursorToStart = function () {
9233         return this.selectRange( 0 );
9237  * Focus the input and move the cursor to the end.
9239  * @chainable
9240  */
9241 OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
9242         return this.selectRange( this.getInputLength() );
9246  * Insert new content into the input.
9248  * @param {string} content Content to be inserted
9249  * @chainable
9250  */
9251 OO.ui.TextInputWidget.prototype.insertContent = function ( content ) {
9252         var start, end,
9253                 range = this.getRange(),
9254                 value = this.getValue();
9256         start = Math.min( range.from, range.to );
9257         end = Math.max( range.from, range.to );
9259         this.setValue( value.slice( 0, start ) + content + value.slice( end ) );
9260         this.selectRange( start + content.length );
9261         return this;
9265  * Insert new content either side of a selection.
9267  * @param {string} pre Content to be inserted before the selection
9268  * @param {string} post Content to be inserted after the selection
9269  * @chainable
9270  */
9271 OO.ui.TextInputWidget.prototype.encapsulateContent = function ( pre, post ) {
9272         var start, end,
9273                 range = this.getRange(),
9274                 offset = pre.length;
9276         start = Math.min( range.from, range.to );
9277         end = Math.max( range.from, range.to );
9279         this.selectRange( start ).insertContent( pre );
9280         this.selectRange( offset + end ).insertContent( post );
9282         this.selectRange( offset + start, offset + end );
9283         return this;
9287  * Set the validation pattern.
9289  * The validation pattern is either a regular expression, a function, or the symbolic name of a
9290  * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
9291  * value must contain only numbers).
9293  * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
9294  *  of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
9295  */
9296 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
9297         if ( validate instanceof RegExp || validate instanceof Function ) {
9298                 this.validate = validate;
9299         } else {
9300                 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
9301         }
9305  * Sets the 'invalid' flag appropriately.
9307  * @param {boolean} [isValid] Optionally override validation result
9308  */
9309 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
9310         var widget = this,
9311                 setFlag = function ( valid ) {
9312                         if ( !valid ) {
9313                                 widget.$input.attr( 'aria-invalid', 'true' );
9314                         } else {
9315                                 widget.$input.removeAttr( 'aria-invalid' );
9316                         }
9317                         widget.setFlags( { invalid: !valid } );
9318                 };
9320         if ( isValid !== undefined ) {
9321                 setFlag( isValid );
9322         } else {
9323                 this.getValidity().then( function () {
9324                         setFlag( true );
9325                 }, function () {
9326                         setFlag( false );
9327                 } );
9328         }
9332  * Get the validity of current value.
9334  * This method returns a promise that resolves if the value is valid and rejects if
9335  * it isn't. Uses the {@link #validate validation pattern}  to check for validity.
9337  * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
9338  */
9339 OO.ui.TextInputWidget.prototype.getValidity = function () {
9340         var result;
9342         function rejectOrResolve( valid ) {
9343                 if ( valid ) {
9344                         return $.Deferred().resolve().promise();
9345                 } else {
9346                         return $.Deferred().reject().promise();
9347                 }
9348         }
9350         if ( this.validate instanceof Function ) {
9351                 result = this.validate( this.getValue() );
9352                 if ( result && $.isFunction( result.promise ) ) {
9353                         return result.promise().then( function ( valid ) {
9354                                 return rejectOrResolve( valid );
9355                         } );
9356                 } else {
9357                         return rejectOrResolve( result );
9358                 }
9359         } else {
9360                 return rejectOrResolve( this.getValue().match( this.validate ) );
9361         }
9365  * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
9367  * @param {string} labelPosition Label position, 'before' or 'after'
9368  * @chainable
9369  */
9370 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
9371         this.labelPosition = labelPosition;
9372         if ( this.label ) {
9373                 // If there is no label and we only change the position, #updatePosition is a no-op,
9374                 // but it takes really a lot of work to do nothing.
9375                 this.updatePosition();
9376         }
9377         return this;
9381  * Update the position of the inline label.
9383  * This method is called by #setLabelPosition, and can also be called on its own if
9384  * something causes the label to be mispositioned.
9386  * @chainable
9387  */
9388 OO.ui.TextInputWidget.prototype.updatePosition = function () {
9389         var after = this.labelPosition === 'after';
9391         this.$element
9392                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
9393                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
9395         this.valCache = null;
9396         this.scrollWidth = null;
9397         this.adjustSize();
9398         this.positionLabel();
9400         return this;
9404  * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
9405  * already empty or when it's not editable.
9406  */
9407 OO.ui.TextInputWidget.prototype.updateSearchIndicator = function () {
9408         if ( this.type === 'search' ) {
9409                 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
9410                         this.setIndicator( null );
9411                 } else {
9412                         this.setIndicator( 'clear' );
9413                 }
9414         }
9418  * Position the label by setting the correct padding on the input.
9420  * @private
9421  * @chainable
9422  */
9423 OO.ui.TextInputWidget.prototype.positionLabel = function () {
9424         var after, rtl, property;
9426         if ( this.isWaitingToBeAttached ) {
9427                 // #onElementAttach will be called soon, which calls this method
9428                 return this;
9429         }
9431         // Clear old values
9432         this.$input
9433                 // Clear old values if present
9434                 .css( {
9435                         'padding-right': '',
9436                         'padding-left': ''
9437                 } );
9439         if ( this.label ) {
9440                 this.$element.append( this.$label );
9441         } else {
9442                 this.$label.detach();
9443                 return;
9444         }
9446         after = this.labelPosition === 'after';
9447         rtl = this.$element.css( 'direction' ) === 'rtl';
9448         property = after === rtl ? 'padding-left' : 'padding-right';
9450         this.$input.css( property, this.$label.outerWidth( true ) + ( after ? this.scrollWidth : 0 ) );
9452         return this;
9456  * @inheritdoc
9457  */
9458 OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
9459         OO.ui.TextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
9460         if ( state.scrollTop !== undefined ) {
9461                 this.$input.scrollTop( state.scrollTop );
9462         }
9466  * @class
9467  * @extends OO.ui.TextInputWidget
9469  * @constructor
9470  * @param {Object} [config] Configuration options
9471  */
9472 OO.ui.SearchInputWidget = function OoUiSearchInputWidget( config ) {
9473         config = $.extend( {
9474                 icon: 'search'
9475         }, config );
9477         // Set type to text so that TextInputWidget doesn't
9478         // get stuck in an infinite loop.
9479         config.type = 'text';
9481         // Parent constructor
9482         OO.ui.SearchInputWidget.parent.call( this, config );
9484         // Initialization
9485         this.$element.addClass( 'oo-ui-textInputWidget-type-search' );
9486         this.updateSearchIndicator();
9487         this.connect( this, {
9488                 disable: 'onDisable'
9489         } );
9492 /* Setup */
9494 OO.inheritClass( OO.ui.SearchInputWidget, OO.ui.TextInputWidget );
9496 /* Methods */
9499  * @inheritdoc
9500  * @protected
9501  */
9502 OO.ui.SearchInputWidget.prototype.getInputElement = function () {
9503         return $( '<input>' ).attr( 'type', 'search' );
9507  * @inheritdoc
9508  */
9509 OO.ui.SearchInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
9510         if ( e.which === OO.ui.MouseButtons.LEFT ) {
9511                 // Clear the text field
9512                 this.setValue( '' );
9513                 this.$input[ 0 ].focus();
9514                 return false;
9515         }
9519  * Update the 'clear' indicator displayed on type: 'search' text
9520  * fields, hiding it when the field is already empty or when it's not
9521  * editable.
9522  */
9523 OO.ui.SearchInputWidget.prototype.updateSearchIndicator = function () {
9524         if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
9525                 this.setIndicator( null );
9526         } else {
9527                 this.setIndicator( 'clear' );
9528         }
9532  * @inheritdoc
9533  */
9534 OO.ui.SearchInputWidget.prototype.onChange = function () {
9535         OO.ui.SearchInputWidget.parent.prototype.onChange.call( this );
9536         this.updateSearchIndicator();
9540  * Handle disable events.
9542  * @param {boolean} disabled Element is disabled
9543  * @private
9544  */
9545 OO.ui.SearchInputWidget.prototype.onDisable = function () {
9546         this.updateSearchIndicator();
9550  * @inheritdoc
9551  */
9552 OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) {
9553         OO.ui.SearchInputWidget.parent.prototype.setReadOnly.call( this, state );
9554         this.updateSearchIndicator();
9555         return this;
9559  * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
9560  * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
9561  * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
9563  * - by typing a value in the text input field. If the value exactly matches the value of a menu
9564  *   option, that option will appear to be selected.
9565  * - by choosing a value from the menu. The value of the chosen option will then appear in the text
9566  *   input field.
9568  * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
9570  * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
9572  *     @example
9573  *     // Example: A ComboBoxInputWidget.
9574  *     var comboBox = new OO.ui.ComboBoxInputWidget( {
9575  *         label: 'ComboBoxInputWidget',
9576  *         value: 'Option 1',
9577  *         menu: {
9578  *             items: [
9579  *                 new OO.ui.MenuOptionWidget( {
9580  *                     data: 'Option 1',
9581  *                     label: 'Option One'
9582  *                 } ),
9583  *                 new OO.ui.MenuOptionWidget( {
9584  *                     data: 'Option 2',
9585  *                     label: 'Option Two'
9586  *                 } ),
9587  *                 new OO.ui.MenuOptionWidget( {
9588  *                     data: 'Option 3',
9589  *                     label: 'Option Three'
9590  *                 } ),
9591  *                 new OO.ui.MenuOptionWidget( {
9592  *                     data: 'Option 4',
9593  *                     label: 'Option Four'
9594  *                 } ),
9595  *                 new OO.ui.MenuOptionWidget( {
9596  *                     data: 'Option 5',
9597  *                     label: 'Option Five'
9598  *                 } )
9599  *             ]
9600  *         }
9601  *     } );
9602  *     $( 'body' ).append( comboBox.$element );
9604  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
9606  * @class
9607  * @extends OO.ui.TextInputWidget
9609  * @constructor
9610  * @param {Object} [config] Configuration options
9611  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9612  * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
9613  * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
9614  *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
9615  *  containing `<div>` and has a larger area. By default, the menu uses relative positioning.
9616  */
9617 OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
9618         // Configuration initialization
9619         config = $.extend( {
9620                 autocomplete: false
9621         }, config );
9623         // ComboBoxInputWidget shouldn't support multiline
9624         config.multiline = false;
9626         // Parent constructor
9627         OO.ui.ComboBoxInputWidget.parent.call( this, config );
9629         // Properties
9630         this.$overlay = config.$overlay || this.$element;
9631         this.dropdownButton = new OO.ui.ButtonWidget( {
9632                 classes: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
9633                 indicator: 'down',
9634                 disabled: this.disabled
9635         } );
9636         this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
9637                 {
9638                         widget: this,
9639                         input: this,
9640                         $container: this.$element,
9641                         disabled: this.isDisabled()
9642                 },
9643                 config.menu
9644         ) );
9646         // Events
9647         this.connect( this, {
9648                 change: 'onInputChange',
9649                 enter: 'onInputEnter'
9650         } );
9651         this.dropdownButton.connect( this, {
9652                 click: 'onDropdownButtonClick'
9653         } );
9654         this.menu.connect( this, {
9655                 choose: 'onMenuChoose',
9656                 add: 'onMenuItemsChange',
9657                 remove: 'onMenuItemsChange'
9658         } );
9660         // Initialization
9661         this.$input.attr( {
9662                 role: 'combobox',
9663                 'aria-autocomplete': 'list'
9664         } );
9665         // Do not override options set via config.menu.items
9666         if ( config.options !== undefined ) {
9667                 this.setOptions( config.options );
9668         }
9669         this.$field = $( '<div>' )
9670                 .addClass( 'oo-ui-comboBoxInputWidget-field' )
9671                 .append( this.$input, this.dropdownButton.$element );
9672         this.$element
9673                 .addClass( 'oo-ui-comboBoxInputWidget' )
9674                 .append( this.$field );
9675         this.$overlay.append( this.menu.$element );
9676         this.onMenuItemsChange();
9679 /* Setup */
9681 OO.inheritClass( OO.ui.ComboBoxInputWidget, OO.ui.TextInputWidget );
9683 /* Methods */
9686  * Get the combobox's menu.
9688  * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
9689  */
9690 OO.ui.ComboBoxInputWidget.prototype.getMenu = function () {
9691         return this.menu;
9695  * Get the combobox's text input widget.
9697  * @return {OO.ui.TextInputWidget} Text input widget
9698  */
9699 OO.ui.ComboBoxInputWidget.prototype.getInput = function () {
9700         return this;
9704  * Handle input change events.
9706  * @private
9707  * @param {string} value New value
9708  */
9709 OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
9710         var match = this.menu.getItemFromData( value );
9712         this.menu.selectItem( match );
9713         if ( this.menu.getHighlightedItem() ) {
9714                 this.menu.highlightItem( match );
9715         }
9717         if ( !this.isDisabled() ) {
9718                 this.menu.toggle( true );
9719         }
9723  * Handle input enter events.
9725  * @private
9726  */
9727 OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
9728         if ( !this.isDisabled() ) {
9729                 this.menu.toggle( false );
9730         }
9734  * Handle button click events.
9736  * @private
9737  */
9738 OO.ui.ComboBoxInputWidget.prototype.onDropdownButtonClick = function () {
9739         this.menu.toggle();
9740         this.$input[ 0 ].focus();
9744  * Handle menu choose events.
9746  * @private
9747  * @param {OO.ui.OptionWidget} item Chosen item
9748  */
9749 OO.ui.ComboBoxInputWidget.prototype.onMenuChoose = function ( item ) {
9750         this.setValue( item.getData() );
9754  * Handle menu item change events.
9756  * @private
9757  */
9758 OO.ui.ComboBoxInputWidget.prototype.onMenuItemsChange = function () {
9759         var match = this.menu.getItemFromData( this.getValue() );
9760         this.menu.selectItem( match );
9761         if ( this.menu.getHighlightedItem() ) {
9762                 this.menu.highlightItem( match );
9763         }
9764         this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu.isEmpty() );
9768  * @inheritdoc
9769  */
9770 OO.ui.ComboBoxInputWidget.prototype.setDisabled = function ( disabled ) {
9771         // Parent method
9772         OO.ui.ComboBoxInputWidget.parent.prototype.setDisabled.call( this, disabled );
9774         if ( this.dropdownButton ) {
9775                 this.dropdownButton.setDisabled( this.isDisabled() );
9776         }
9777         if ( this.menu ) {
9778                 this.menu.setDisabled( this.isDisabled() );
9779         }
9781         return this;
9785  * Set the options available for this input.
9787  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9788  * @chainable
9789  */
9790 OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
9791         this.getMenu()
9792                 .clearItems()
9793                 .addItems( options.map( function ( opt ) {
9794                         return new OO.ui.MenuOptionWidget( {
9795                                 data: opt.data,
9796                                 label: opt.label !== undefined ? opt.label : opt.data
9797                         } );
9798                 } ) );
9800         return this;
9804  * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
9805  * which is a widget that is specified by reference before any optional configuration settings.
9807  * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
9809  * - **left**: The label is placed before the field-widget and aligned with the left margin.
9810  *   A left-alignment is used for forms with many fields.
9811  * - **right**: The label is placed before the field-widget and aligned to the right margin.
9812  *   A right-alignment is used for long but familiar forms which users tab through,
9813  *   verifying the current field with a quick glance at the label.
9814  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9815  *   that users fill out from top to bottom.
9816  * - **inline**: The label is placed after the field-widget and aligned to the left.
9817  *   An inline-alignment is best used with checkboxes or radio buttons.
9819  * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
9820  * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
9822  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9824  * @class
9825  * @extends OO.ui.Layout
9826  * @mixins OO.ui.mixin.LabelElement
9827  * @mixins OO.ui.mixin.TitledElement
9829  * @constructor
9830  * @param {OO.ui.Widget} fieldWidget Field widget
9831  * @param {Object} [config] Configuration options
9832  * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
9833  * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
9834  *  The array may contain strings or OO.ui.HtmlSnippet instances.
9835  * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
9836  *  The array may contain strings or OO.ui.HtmlSnippet instances.
9837  * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
9838  *  in the upper-right corner of the rendered field; clicking it will display the text in a popup.
9839  *  For important messages, you are advised to use `notices`, as they are always shown.
9841  * @throws {Error} An error is thrown if no widget is specified
9842  */
9843 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
9844         var hasInputWidget, $div;
9846         // Allow passing positional parameters inside the config object
9847         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
9848                 config = fieldWidget;
9849                 fieldWidget = config.fieldWidget;
9850         }
9852         // Make sure we have required constructor arguments
9853         if ( fieldWidget === undefined ) {
9854                 throw new Error( 'Widget not found' );
9855         }
9857         hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
9859         // Configuration initialization
9860         config = $.extend( { align: 'left' }, config );
9862         // Parent constructor
9863         OO.ui.FieldLayout.parent.call( this, config );
9865         // Mixin constructors
9866         OO.ui.mixin.LabelElement.call( this, config );
9867         OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
9869         // Properties
9870         this.fieldWidget = fieldWidget;
9871         this.errors = [];
9872         this.notices = [];
9873         this.$field = $( '<div>' );
9874         this.$messages = $( '<ul>' );
9875         this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
9876         this.align = null;
9877         if ( config.help ) {
9878                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
9879                         classes: [ 'oo-ui-fieldLayout-help' ],
9880                         framed: false,
9881                         icon: 'info'
9882                 } );
9884                 $div = $( '<div>' );
9885                 if ( config.help instanceof OO.ui.HtmlSnippet ) {
9886                         $div.html( config.help.toString() );
9887                 } else {
9888                         $div.text( config.help );
9889                 }
9890                 this.popupButtonWidget.getPopup().$body.append(
9891                         $div.addClass( 'oo-ui-fieldLayout-help-content' )
9892                 );
9893                 this.$help = this.popupButtonWidget.$element;
9894         } else {
9895                 this.$help = $( [] );
9896         }
9898         // Events
9899         if ( hasInputWidget ) {
9900                 this.$label.on( 'click', this.onLabelClick.bind( this ) );
9901         }
9902         this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
9904         // Initialization
9905         this.$element
9906                 .addClass( 'oo-ui-fieldLayout' )
9907                 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget.isDisabled() )
9908                 .append( this.$help, this.$body );
9909         this.$body.addClass( 'oo-ui-fieldLayout-body' );
9910         this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
9911         this.$field
9912                 .addClass( 'oo-ui-fieldLayout-field' )
9913                 .append( this.fieldWidget.$element );
9915         this.setErrors( config.errors || [] );
9916         this.setNotices( config.notices || [] );
9917         this.setAlignment( config.align );
9920 /* Setup */
9922 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
9923 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
9924 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.TitledElement );
9926 /* Methods */
9929  * Handle field disable events.
9931  * @private
9932  * @param {boolean} value Field is disabled
9933  */
9934 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
9935         this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
9939  * Handle label mouse click events.
9941  * @private
9942  * @param {jQuery.Event} e Mouse click event
9943  */
9944 OO.ui.FieldLayout.prototype.onLabelClick = function () {
9945         this.fieldWidget.simulateLabelClick();
9946         return false;
9950  * Get the widget contained by the field.
9952  * @return {OO.ui.Widget} Field widget
9953  */
9954 OO.ui.FieldLayout.prototype.getField = function () {
9955         return this.fieldWidget;
9959  * @protected
9960  * @param {string} kind 'error' or 'notice'
9961  * @param {string|OO.ui.HtmlSnippet} text
9962  * @return {jQuery}
9963  */
9964 OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
9965         var $listItem, $icon, message;
9966         $listItem = $( '<li>' );
9967         if ( kind === 'error' ) {
9968                 $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
9969         } else if ( kind === 'notice' ) {
9970                 $icon = new OO.ui.IconWidget( { icon: 'info' } ).$element;
9971         } else {
9972                 $icon = '';
9973         }
9974         message = new OO.ui.LabelWidget( { label: text } );
9975         $listItem
9976                 .append( $icon, message.$element )
9977                 .addClass( 'oo-ui-fieldLayout-messages-' + kind );
9978         return $listItem;
9982  * Set the field alignment mode.
9984  * @private
9985  * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
9986  * @chainable
9987  */
9988 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
9989         if ( value !== this.align ) {
9990                 // Default to 'left'
9991                 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
9992                         value = 'left';
9993                 }
9994                 // Reorder elements
9995                 if ( value === 'inline' ) {
9996                         this.$body.append( this.$field, this.$label );
9997                 } else {
9998                         this.$body.append( this.$label, this.$field );
9999                 }
10000                 // Set classes. The following classes can be used here:
10001                 // * oo-ui-fieldLayout-align-left
10002                 // * oo-ui-fieldLayout-align-right
10003                 // * oo-ui-fieldLayout-align-top
10004                 // * oo-ui-fieldLayout-align-inline
10005                 if ( this.align ) {
10006                         this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
10007                 }
10008                 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
10009                 this.align = value;
10010         }
10012         return this;
10016  * Set the list of error messages.
10018  * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
10019  *  The array may contain strings or OO.ui.HtmlSnippet instances.
10020  * @chainable
10021  */
10022 OO.ui.FieldLayout.prototype.setErrors = function ( errors ) {
10023         this.errors = errors.slice();
10024         this.updateMessages();
10025         return this;
10029  * Set the list of notice messages.
10031  * @param {Array} notices Notices about the widget, which will be displayed below the widget.
10032  *  The array may contain strings or OO.ui.HtmlSnippet instances.
10033  * @chainable
10034  */
10035 OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
10036         this.notices = notices.slice();
10037         this.updateMessages();
10038         return this;
10042  * Update the rendering of error and notice messages.
10044  * @private
10045  */
10046 OO.ui.FieldLayout.prototype.updateMessages = function () {
10047         var i;
10048         this.$messages.empty();
10050         if ( this.errors.length || this.notices.length ) {
10051                 this.$body.after( this.$messages );
10052         } else {
10053                 this.$messages.remove();
10054                 return;
10055         }
10057         for ( i = 0; i < this.notices.length; i++ ) {
10058                 this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
10059         }
10060         for ( i = 0; i < this.errors.length; i++ ) {
10061                 this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
10062         }
10066  * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
10067  * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
10068  * is required and is specified before any optional configuration settings.
10070  * Labels can be aligned in one of four ways:
10072  * - **left**: The label is placed before the field-widget and aligned with the left margin.
10073  *   A left-alignment is used for forms with many fields.
10074  * - **right**: The label is placed before the field-widget and aligned to the right margin.
10075  *   A right-alignment is used for long but familiar forms which users tab through,
10076  *   verifying the current field with a quick glance at the label.
10077  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
10078  *   that users fill out from top to bottom.
10079  * - **inline**: The label is placed after the field-widget and aligned to the left.
10080  *   An inline-alignment is best used with checkboxes or radio buttons.
10082  * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
10083  * text is specified.
10085  *     @example
10086  *     // Example of an ActionFieldLayout
10087  *     var actionFieldLayout = new OO.ui.ActionFieldLayout(
10088  *         new OO.ui.TextInputWidget( {
10089  *             placeholder: 'Field widget'
10090  *         } ),
10091  *         new OO.ui.ButtonWidget( {
10092  *             label: 'Button'
10093  *         } ),
10094  *         {
10095  *             label: 'An ActionFieldLayout. This label is aligned top',
10096  *             align: 'top',
10097  *             help: 'This is help text'
10098  *         }
10099  *     );
10101  *     $( 'body' ).append( actionFieldLayout.$element );
10103  * @class
10104  * @extends OO.ui.FieldLayout
10106  * @constructor
10107  * @param {OO.ui.Widget} fieldWidget Field widget
10108  * @param {OO.ui.ButtonWidget} buttonWidget Button widget
10109  * @param {Object} config
10110  */
10111 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
10112         // Allow passing positional parameters inside the config object
10113         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
10114                 config = fieldWidget;
10115                 fieldWidget = config.fieldWidget;
10116                 buttonWidget = config.buttonWidget;
10117         }
10119         // Parent constructor
10120         OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
10122         // Properties
10123         this.buttonWidget = buttonWidget;
10124         this.$button = $( '<div>' );
10125         this.$input = $( '<div>' );
10127         // Initialization
10128         this.$element
10129                 .addClass( 'oo-ui-actionFieldLayout' );
10130         this.$button
10131                 .addClass( 'oo-ui-actionFieldLayout-button' )
10132                 .append( this.buttonWidget.$element );
10133         this.$input
10134                 .addClass( 'oo-ui-actionFieldLayout-input' )
10135                 .append( this.fieldWidget.$element );
10136         this.$field
10137                 .append( this.$input, this.$button );
10140 /* Setup */
10142 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
10145  * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
10146  * which each contain an individual widget and, optionally, a label. Each Fieldset can be
10147  * configured with a label as well. For more information and examples,
10148  * please see the [OOjs UI documentation on MediaWiki][1].
10150  *     @example
10151  *     // Example of a fieldset layout
10152  *     var input1 = new OO.ui.TextInputWidget( {
10153  *         placeholder: 'A text input field'
10154  *     } );
10156  *     var input2 = new OO.ui.TextInputWidget( {
10157  *         placeholder: 'A text input field'
10158  *     } );
10160  *     var fieldset = new OO.ui.FieldsetLayout( {
10161  *         label: 'Example of a fieldset layout'
10162  *     } );
10164  *     fieldset.addItems( [
10165  *         new OO.ui.FieldLayout( input1, {
10166  *             label: 'Field One'
10167  *         } ),
10168  *         new OO.ui.FieldLayout( input2, {
10169  *             label: 'Field Two'
10170  *         } )
10171  *     ] );
10172  *     $( 'body' ).append( fieldset.$element );
10174  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
10176  * @class
10177  * @extends OO.ui.Layout
10178  * @mixins OO.ui.mixin.IconElement
10179  * @mixins OO.ui.mixin.LabelElement
10180  * @mixins OO.ui.mixin.GroupElement
10182  * @constructor
10183  * @param {Object} [config] Configuration options
10184  * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
10185  * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
10186  *  in the upper-right corner of the rendered field; clicking it will display the text in a popup.
10187  *  For important messages, you are advised to use `notices`, as they are always shown.
10188  */
10189 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
10190         var $div;
10192         // Configuration initialization
10193         config = config || {};
10195         // Parent constructor
10196         OO.ui.FieldsetLayout.parent.call( this, config );
10198         // Mixin constructors
10199         OO.ui.mixin.IconElement.call( this, config );
10200         OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: $( '<div>' ) } ) );
10201         OO.ui.mixin.GroupElement.call( this, config );
10203         if ( config.help ) {
10204                 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
10205                         classes: [ 'oo-ui-fieldsetLayout-help' ],
10206                         framed: false,
10207                         icon: 'info'
10208                 } );
10210                 $div = $( '<div>' );
10211                 if ( config.help instanceof OO.ui.HtmlSnippet ) {
10212                         $div.html( config.help.toString() );
10213                 } else {
10214                         $div.text( config.help );
10215                 }
10216                 this.popupButtonWidget.getPopup().$body.append(
10217                         $div.addClass( 'oo-ui-fieldsetLayout-help-content' )
10218                 );
10219                 this.$help = this.popupButtonWidget.$element;
10220         } else {
10221                 this.$help = $( [] );
10222         }
10224         // Initialization
10225         this.$group.addClass( 'oo-ui-fieldsetLayout-group' );
10226         this.$element
10227                 .addClass( 'oo-ui-fieldsetLayout' )
10228                 .prepend( this.$label, this.$help, this.$icon, this.$group );
10229         if ( Array.isArray( config.items ) ) {
10230                 this.addItems( config.items );
10231         }
10234 /* Setup */
10236 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
10237 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
10238 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
10239 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
10241 /* Static Properties */
10243 OO.ui.FieldsetLayout.static.tagName = 'fieldset';
10246  * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
10247  * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
10248  * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
10249  * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10251  * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
10252  * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
10253  * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
10254  * some fancier controls. Some controls have both regular and InputWidget variants, for example
10255  * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
10256  * often have simplified APIs to match the capabilities of HTML forms.
10257  * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
10259  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
10260  * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10262  *     @example
10263  *     // Example of a form layout that wraps a fieldset layout
10264  *     var input1 = new OO.ui.TextInputWidget( {
10265  *         placeholder: 'Username'
10266  *     } );
10267  *     var input2 = new OO.ui.TextInputWidget( {
10268  *         placeholder: 'Password',
10269  *         type: 'password'
10270  *     } );
10271  *     var submit = new OO.ui.ButtonInputWidget( {
10272  *         label: 'Submit'
10273  *     } );
10275  *     var fieldset = new OO.ui.FieldsetLayout( {
10276  *         label: 'A form layout'
10277  *     } );
10278  *     fieldset.addItems( [
10279  *         new OO.ui.FieldLayout( input1, {
10280  *             label: 'Username',
10281  *             align: 'top'
10282  *         } ),
10283  *         new OO.ui.FieldLayout( input2, {
10284  *             label: 'Password',
10285  *             align: 'top'
10286  *         } ),
10287  *         new OO.ui.FieldLayout( submit )
10288  *     ] );
10289  *     var form = new OO.ui.FormLayout( {
10290  *         items: [ fieldset ],
10291  *         action: '/api/formhandler',
10292  *         method: 'get'
10293  *     } )
10294  *     $( 'body' ).append( form.$element );
10296  * @class
10297  * @extends OO.ui.Layout
10298  * @mixins OO.ui.mixin.GroupElement
10300  * @constructor
10301  * @param {Object} [config] Configuration options
10302  * @cfg {string} [method] HTML form `method` attribute
10303  * @cfg {string} [action] HTML form `action` attribute
10304  * @cfg {string} [enctype] HTML form `enctype` attribute
10305  * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
10306  */
10307 OO.ui.FormLayout = function OoUiFormLayout( config ) {
10308         var action;
10310         // Configuration initialization
10311         config = config || {};
10313         // Parent constructor
10314         OO.ui.FormLayout.parent.call( this, config );
10316         // Mixin constructors
10317         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10319         // Events
10320         this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
10322         // Make sure the action is safe
10323         action = config.action;
10324         if ( action !== undefined && !OO.ui.isSafeUrl( action ) ) {
10325                 action = './' + action;
10326         }
10328         // Initialization
10329         this.$element
10330                 .addClass( 'oo-ui-formLayout' )
10331                 .attr( {
10332                         method: config.method,
10333                         action: action,
10334                         enctype: config.enctype
10335                 } );
10336         if ( Array.isArray( config.items ) ) {
10337                 this.addItems( config.items );
10338         }
10341 /* Setup */
10343 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
10344 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
10346 /* Events */
10349  * A 'submit' event is emitted when the form is submitted.
10351  * @event submit
10352  */
10354 /* Static Properties */
10356 OO.ui.FormLayout.static.tagName = 'form';
10358 /* Methods */
10361  * Handle form submit events.
10363  * @private
10364  * @param {jQuery.Event} e Submit event
10365  * @fires submit
10366  */
10367 OO.ui.FormLayout.prototype.onFormSubmit = function () {
10368         if ( this.emit( 'submit' ) ) {
10369                 return false;
10370         }
10374  * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10375  * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10377  *     @example
10378  *     // Example of a panel layout
10379  *     var panel = new OO.ui.PanelLayout( {
10380  *         expanded: false,
10381  *         framed: true,
10382  *         padded: true,
10383  *         $content: $( '<p>A panel layout with padding and a frame.</p>' )
10384  *     } );
10385  *     $( 'body' ).append( panel.$element );
10387  * @class
10388  * @extends OO.ui.Layout
10390  * @constructor
10391  * @param {Object} [config] Configuration options
10392  * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10393  * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10394  * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10395  * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10396  */
10397 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
10398         // Configuration initialization
10399         config = $.extend( {
10400                 scrollable: false,
10401                 padded: false,
10402                 expanded: true,
10403                 framed: false
10404         }, config );
10406         // Parent constructor
10407         OO.ui.PanelLayout.parent.call( this, config );
10409         // Initialization
10410         this.$element.addClass( 'oo-ui-panelLayout' );
10411         if ( config.scrollable ) {
10412                 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
10413         }
10414         if ( config.padded ) {
10415                 this.$element.addClass( 'oo-ui-panelLayout-padded' );
10416         }
10417         if ( config.expanded ) {
10418                 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
10419         }
10420         if ( config.framed ) {
10421                 this.$element.addClass( 'oo-ui-panelLayout-framed' );
10422         }
10425 /* Setup */
10427 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
10429 /* Methods */
10432  * Focus the panel layout
10434  * The default implementation just focuses the first focusable element in the panel
10435  */
10436 OO.ui.PanelLayout.prototype.focus = function () {
10437         OO.ui.findFocusable( this.$element ).focus();
10441  * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
10442  * items), with small margins between them. Convenient when you need to put a number of block-level
10443  * widgets on a single line next to each other.
10445  * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
10447  *     @example
10448  *     // HorizontalLayout with a text input and a label
10449  *     var layout = new OO.ui.HorizontalLayout( {
10450  *       items: [
10451  *         new OO.ui.LabelWidget( { label: 'Label' } ),
10452  *         new OO.ui.TextInputWidget( { value: 'Text' } )
10453  *       ]
10454  *     } );
10455  *     $( 'body' ).append( layout.$element );
10457  * @class
10458  * @extends OO.ui.Layout
10459  * @mixins OO.ui.mixin.GroupElement
10461  * @constructor
10462  * @param {Object} [config] Configuration options
10463  * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
10464  */
10465 OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
10466         // Configuration initialization
10467         config = config || {};
10469         // Parent constructor
10470         OO.ui.HorizontalLayout.parent.call( this, config );
10472         // Mixin constructors
10473         OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10475         // Initialization
10476         this.$element.addClass( 'oo-ui-horizontalLayout' );
10477         if ( Array.isArray( config.items ) ) {
10478                 this.addItems( config.items );
10479         }
10482 /* Setup */
10484 OO.inheritClass( OO.ui.HorizontalLayout, OO.ui.Layout );
10485 OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
10487 }( OO ) );