Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / lib / ooui / oojs-ui-core.js
blobe6c7797d737d9c476f2cbbde132c04ddd963e77d
1 /*!
2  * OOUI v0.51.4
3  * https://www.mediawiki.org/wiki/OOUI
4  *
5  * Copyright 2011–2024 OOUI Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2024-12-05T17:34:41Z
10  */
11 ( function ( OO ) {
13 'use strict';
15 /**
16  * Namespace for all classes, static methods and static properties.
17  *
18  * @namespace
19  */
20 OO.ui = {};
22 OO.ui.bind = $.proxy;
24 /**
25  * @property {Object}
26  */
27 OO.ui.Keys = {
28         UNDEFINED: 0,
29         BACKSPACE: 8,
30         DELETE: 46,
31         LEFT: 37,
32         RIGHT: 39,
33         UP: 38,
34         DOWN: 40,
35         ENTER: 13,
36         END: 35,
37         HOME: 36,
38         TAB: 9,
39         PAGEUP: 33,
40         PAGEDOWN: 34,
41         ESCAPE: 27,
42         SHIFT: 16,
43         SPACE: 32
46 /**
47  * Constants for MouseEvent.which
48  *
49  * @property {Object}
50  */
51 OO.ui.MouseButtons = {
52         LEFT: 1,
53         MIDDLE: 2,
54         RIGHT: 3
57 /**
58  * @property {number}
59  * @private
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++;
70         return 'ooui-' + OO.ui.elementId;
73 /**
74  * Check if an element is focusable.
75  * Inspired by :focusable in jQueryUI v1.11.4 - 2015-04-14
76  *
77  * @param {jQuery} $element Element to test
78  * @return {boolean} Element is focusable
79  */
80 OO.ui.isFocusableElement = function ( $element ) {
81         const element = $element[ 0 ];
83         // Anything disabled is not focusable
84         if ( element.disabled ) {
85                 return false;
86         }
88         // Check if the element is visible
89         if ( !(
90                 // This is quicker than calling $element.is( ':visible' )
91                 $.expr.pseudos.visible( element ) &&
92                 // Check that all parents are visible
93                 !$element.parents().addBack().filter( function () {
94                         return $.css( this, 'visibility' ) === 'hidden';
95                 } ).length
96         ) ) {
97                 return false;
98         }
100         // Check if the element is ContentEditable, which is the string 'true'
101         if ( element.contentEditable === 'true' ) {
102                 return true;
103         }
105         // Anything with a non-negative numeric tabIndex is focusable.
106         // Use .prop to avoid browser bugs
107         if ( $element.prop( 'tabIndex' ) >= 0 ) {
108                 return true;
109         }
111         // Some element types are naturally focusable
112         // (indexOf is much faster than regex in Chrome and about the
113         // same in FF: https://jsperf.com/regex-vs-indexof-array2)
114         const nodeName = element.nodeName.toLowerCase();
115         if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName ) !== -1 ) {
116                 return true;
117         }
119         // Links and areas are focusable if they have an href
120         if ( ( nodeName === 'a' || nodeName === 'area' ) && $element.attr( 'href' ) !== undefined ) {
121                 return true;
122         }
124         return false;
128  * Find a focusable child.
130  * @param {jQuery} $container Container to search in
131  * @param {boolean} [backwards=false] Search backwards
132  * @return {jQuery} Focusable child, or an empty jQuery object if none found
133  */
134 OO.ui.findFocusable = function ( $container, backwards ) {
135         let $focusable = $( [] ),
136                 // $focusableCandidates is a superset of things that
137                 // could get matched by isFocusableElement
138                 $focusableCandidates = $container
139                         .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
141         if ( backwards ) {
142                 $focusableCandidates = Array.prototype.reverse.call( $focusableCandidates );
143         }
145         $focusableCandidates.each( ( i, el ) => {
146                 const $el = $( el );
147                 if ( OO.ui.isFocusableElement( $el ) ) {
148                         $focusable = $el;
149                         return false;
150                 }
151         } );
152         return $focusable;
156  * Get the user's language and any fallback languages.
158  * These language codes are used to localize user interface elements in the user's language.
160  * In environments that provide a localization system, this function should be overridden to
161  * return the user's language(s). The default implementation returns English (en) only.
163  * @return {string[]} Language codes, in descending order of priority
164  */
165 OO.ui.getUserLanguages = function () {
166         return [ 'en' ];
170  * Get a value in an object keyed by language code.
172  * @param {Object.<string,any>} obj Object keyed by language code
173  * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
174  * @param {string} [fallback] Fallback code, used if no matching language can be found
175  * @return {any} Local value
176  */
177 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
178         // Requested language
179         if ( obj[ lang ] ) {
180                 return obj[ lang ];
181         }
182         // Known user language
183         const langs = OO.ui.getUserLanguages();
184         for ( let i = 0, len = langs.length; i < len; i++ ) {
185                 lang = langs[ i ];
186                 if ( obj[ lang ] ) {
187                         return obj[ lang ];
188                 }
189         }
190         // Fallback language
191         if ( obj[ fallback ] ) {
192                 return obj[ fallback ];
193         }
194         // First existing language
195         // eslint-disable-next-line no-unreachable-loop
196         for ( lang in obj ) {
197                 return obj[ lang ];
198         }
200         return undefined;
204  * Check if a node is contained within another node.
206  * Similar to jQuery#contains except a list of containers can be supplied
207  * and a boolean argument allows you to include the container in the match list
209  * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
210  * @param {HTMLElement} contained Node to find
211  * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match,
212  *  otherwise only match descendants
213  * @return {boolean} The node is in the list of target nodes
214  */
215 OO.ui.contains = function ( containers, contained, matchContainers ) {
216         if ( !Array.isArray( containers ) ) {
217                 containers = [ containers ];
218         }
219         for ( let i = containers.length - 1; i >= 0; i-- ) {
220                 if (
221                         ( matchContainers && contained === containers[ i ] ) ||
222                         $.contains( containers[ i ], contained )
223                 ) {
224                         return true;
225                 }
226         }
227         return false;
231  * Return a function, that, as long as it continues to be invoked, will not
232  * be triggered. The function will be called after it stops being called for
233  * N milliseconds. If `immediate` is passed, trigger the function on the
234  * leading edge, instead of the trailing.
236  * Ported from: http://underscorejs.org/underscore.js
238  * @param {Function} func Function to debounce
239  * @param {number} [wait=0] Wait period in milliseconds
240  * @param {boolean} [immediate] Trigger on leading edge
241  * @return {Function} Debounced function
242  */
243 OO.ui.debounce = function ( func, wait, immediate ) {
244         let timeout;
245         return function () {
246                 const context = this,
247                         args = arguments,
248                         later = function () {
249                                 timeout = null;
250                                 if ( !immediate ) {
251                                         func.apply( context, args );
252                                 }
253                         };
254                 if ( immediate && !timeout ) {
255                         func.apply( context, args );
256                 }
257                 if ( !timeout || wait ) {
258                         clearTimeout( timeout );
259                         timeout = setTimeout( later, wait );
260                 }
261         };
265  * Puts a console warning with provided message.
267  * @param {string} message Message
268  */
269 OO.ui.warnDeprecation = function ( message ) {
270         if ( OO.getProp( window, 'console', 'warn' ) !== undefined ) {
271                 // eslint-disable-next-line no-console
272                 console.warn( message );
273         }
277  * Returns a function, that, when invoked, will only be triggered at most once
278  * during a given window of time. If called again during that window, it will
279  * wait until the window ends and then trigger itself again.
281  * As it's not knowable to the caller whether the function will actually run
282  * when the wrapper is called, return values from the function are entirely
283  * discarded.
285  * @param {Function} func Function to throttle
286  * @param {number} wait Throttle window length, in milliseconds
287  * @return {Function} Throttled function
288  */
289 OO.ui.throttle = function ( func, wait ) {
290         let context, args, timeout,
291                 previous = Date.now() - wait;
293         const run = function () {
294                 timeout = null;
295                 previous = Date.now();
296                 func.apply( context, args );
297         };
299         return function () {
300                 // Check how long it's been since the last time the function was
301                 // called, and whether it's more or less than the requested throttle
302                 // period. If it's less, run the function immediately. If it's more,
303                 // set a timeout for the remaining time -- but don't replace an
304                 // existing timeout, since that'd indefinitely prolong the wait.
305                 const remaining = Math.max( wait - ( Date.now() - previous ), 0 );
306                 context = this;
307                 args = arguments;
308                 if ( !timeout ) {
309                         // If time is up, do setTimeout( run, 0 ) so the function
310                         // always runs asynchronously, just like Promise#then .
311                         timeout = setTimeout( run, remaining );
312                 }
313         };
317  * Reconstitute a JavaScript object corresponding to a widget created by
318  * the PHP implementation.
320  * This is an alias for `OO.ui.Element.static.infuse()`.
322  * @param {string|HTMLElement|jQuery} node A single node for the widget to infuse.
323  *   String must be a selector (deprecated).
324  * @param {Object} [config] Configuration options
325  * @return {OO.ui.Element}
326  *   The `OO.ui.Element` corresponding to this (infusable) document node.
327  */
328 OO.ui.infuse = function ( node, config ) {
329         if ( typeof node === 'string' ) {
330                 // Deprecate passing a selector, which was accidentally introduced in Ibf95b0dee.
331                 // @since 0.41.0
332                 OO.ui.warnDeprecation(
333                         'Passing a selector to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
334                 );
335         }
336         return OO.ui.Element.static.infuse( node, config );
340  * Get a localized message.
342  * After the message key, message parameters may optionally be passed. In the default
343  * implementation, any occurrences of $1 are replaced with the first parameter, $2 with the
344  * second parameter, etc.
345  * Alternative implementations of OO.ui.msg may use any substitution system they like, as long
346  * as they support unnamed, ordered message parameters.
348  * In environments that provide a localization system, this function should be overridden to
349  * return the message translated in the user's language. The default implementation always
350  * returns English messages. An example of doing this with
351  * [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) follows.
353  *     @example
354  *     const messagePath = 'oojs-ui/dist/i18n/',
355  *         languages = [ $.i18n().locale, 'ur', 'en' ],
356  *         languageMap = {};
358  *     for ( let i = 0, iLen = languages.length; i < iLen; i++ ) {
359  *         languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
360  *     }
362  *     $.i18n().load( languageMap ).done( function() {
363  *         // Replace the built-in `msg` only once we've loaded the internationalization.
364  *         // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
365  *         // you put off creating any widgets until this promise is complete, no English
366  *         // will be displayed.
367  *         OO.ui.msg = $.i18n;
369  *         // A button displaying "OK" in the default locale
370  *         const button = new OO.ui.ButtonWidget( {
371  *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
372  *             icon: 'check'
373  *         } );
374  *         $( document.body ).append( button.$element );
376  *         // A button displaying "OK" in Urdu
377  *         $.i18n().locale = 'ur';
378  *         button = new OO.ui.ButtonWidget( {
379  *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
380  *             icon: 'check'
381  *         } );
382  *         $( document.body ).append( button.$element );
383  *     } );
385  * @param {string} key Message key
386  * @param {...any} [params] Message parameters
387  * @return {string} Translated message with parameters substituted
388  */
389 OO.ui.msg = function ( key, ...params ) {
390         // `OO.ui.msg.messages` is defined in code generated during the build process
391         const messages = OO.ui.msg.messages;
393         let message = messages[ key ];
394         if ( typeof message === 'string' ) {
395                 // Perform $1 substitution
396                 message = message.replace( /\$(\d+)/g, ( unused, n ) => {
397                         const i = parseInt( n, 10 );
398                         return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
399                 } );
400         } else {
401                 // Return placeholder if message not found
402                 message = '[' + key + ']';
403         }
404         return message;
408  * Package a message and arguments for deferred resolution.
410  * Use this when you are statically specifying a message and the message may not yet be present.
412  * @param {string} key Message key
413  * @param {...any} [params] Message parameters
414  * @return {Function} Function that returns the resolved message when executed
415  */
416 OO.ui.deferMsg = function () {
417         // eslint-disable-next-line mediawiki/msg-doc
418         return () => OO.ui.msg( ...arguments );
422  * Resolve a message.
424  * If the message is a function it will be executed, otherwise it will pass through directly.
426  * @param {Function|string|any} msg
427  * @return {string|any} Resolved message when there was something to resolve, pass through
428  *  otherwise
429  */
430 OO.ui.resolveMsg = function ( msg ) {
431         if ( typeof msg === 'function' ) {
432                 return msg();
433         }
434         return msg;
438  * @param {string} url
439  * @return {boolean}
440  */
441 OO.ui.isSafeUrl = function ( url ) {
442         // Keep this function in sync with php/Tag.php
444         function stringStartsWith( haystack, needle ) {
445                 return haystack.slice( 0, needle.length ) === needle;
446         }
448         const protocolAllowList = [
449                 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
450                 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
451                 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
452         ];
454         if ( url === '' ) {
455                 return true;
456         }
458         for ( let i = 0; i < protocolAllowList.length; i++ ) {
459                 if ( stringStartsWith( url, protocolAllowList[ i ] + ':' ) ) {
460                         return true;
461                 }
462         }
464         // This matches '//' too
465         if ( stringStartsWith( url, '/' ) || stringStartsWith( url, './' ) ) {
466                 return true;
467         }
468         if ( stringStartsWith( url, '?' ) || stringStartsWith( url, '#' ) ) {
469                 return true;
470         }
472         return false;
476  * Check if the user has a 'mobile' device.
478  * For our purposes this means the user is primarily using an
479  * on-screen keyboard, touch input instead of a mouse and may
480  * have a physically small display.
482  * It is left up to implementors to decide how to compute this
483  * so the default implementation always returns false.
485  * @return {boolean} User is on a mobile device
486  */
487 OO.ui.isMobile = function () {
488         return false;
492  * Get the additional spacing that should be taken into account when displaying elements that are
493  * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
494  * such menus overlapping any fixed headers/toolbars/navigation used by the site.
496  * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
497  *  the extra spacing from that edge of viewport (in pixels)
498  */
499 OO.ui.getViewportSpacing = function () {
500         return {
501                 top: 0,
502                 right: 0,
503                 bottom: 0,
504                 left: 0
505         };
509  * Get the element where elements that are positioned outside of normal flow are inserted,
510  * for example dialogs and dropdown menus.
512  * This is meant to be overridden if the site needs to style this element in some way
513  * (e.g. setting font size), and doesn't want to style the whole document.
515  * @return {HTMLElement}
516  */
517 OO.ui.getTeleportTarget = function () {
518         return document.body;
522  * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
523  * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
525  * @return {jQuery} Default overlay node
526  */
527 OO.ui.getDefaultOverlay = function () {
528         if ( !OO.ui.$defaultOverlay ) {
529                 OO.ui.$defaultOverlay = $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
530                 $( OO.ui.getTeleportTarget() ).append( OO.ui.$defaultOverlay );
531         }
532         return OO.ui.$defaultOverlay;
535 // Define a custom HTML element that does nothing except to expose the `connectedCallback` callback
536 // as `onConnectOOUI` property. We use it in some widgets to detect when they are connected.
537 if ( window.customElements ) {
538         window.customElements.define( 'ooui-connect-detector', class extends HTMLElement {
539                 connectedCallback() {
540                         if ( this.onConnectOOUI instanceof Function ) {
541                                 this.onConnectOOUI();
542                         }
543                 }
544         } );
548  * Message store for the default implementation of OO.ui.msg.
550  * Environments that provide a localization system should not use this, but should override
551  * OO.ui.msg altogether.
553  * @private
554  */
555 OO.ui.msg.messages = {
556         "ooui-copytextlayout-copy": "Copy",
557         "ooui-outline-control-move-down": "Move item down",
558         "ooui-outline-control-move-up": "Move item up",
559         "ooui-outline-control-remove": "Remove item",
560         "ooui-toolbar-more": "More",
561         "ooui-toolgroup-expand": "More",
562         "ooui-toolgroup-collapse": "Fewer",
563         "ooui-item-remove": "Remove",
564         "ooui-dialog-message-accept": "OK",
565         "ooui-dialog-message-reject": "Cancel",
566         "ooui-dialog-process-error": "Something went wrong",
567         "ooui-dialog-process-dismiss": "Dismiss",
568         "ooui-dialog-process-retry": "Try again",
569         "ooui-dialog-process-continue": "Continue",
570         "ooui-combobox-button-label": "Toggle options",
571         "ooui-selectfile-button-select": "Select a file",
572         "ooui-selectfile-button-select-multiple": "Select files",
573         "ooui-selectfile-placeholder": "No file is selected",
574         "ooui-selectfile-dragdrop-placeholder": "Drop file here",
575         "ooui-selectfile-dragdrop-placeholder-multiple": "Drop files here",
576         "ooui-popup-widget-close-button-aria-label": "Close",
577         "ooui-field-help": "Help"
581  * Mixin namespace.
582  */
585  * Namespace for OOUI mixins.
587  * Mixins are named according to the type of object they are intended to
588  * be mixed in to.  For example, OO.ui.mixin.GroupElement is intended to be
589  * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
590  * is intended to be mixed in to an instance of OO.ui.Widget.
592  * @namespace
593  */
594 OO.ui.mixin = {};
596 // getDocument( element ) is preferrable to window.document
597 /* global document:off */
600  * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
601  * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not
602  * have events connected to them and can't be interacted with.
604  * @abstract
605  * @class
607  * @constructor
608  * @param {Object} [config] Configuration options
609  * @param {string[]} [config.classes] The names of the CSS classes to apply to the element. CSS styles are
610  *  added to the top level (e.g., the outermost div) of the element. See the
611  *  [OOUI documentation on MediaWiki][2] for an example.
612  *  [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
613  * @param {string} [config.id] The HTML id attribute used in the rendered tag.
614  * @param {string} [config.text] Text to insert
615  * @param {Array} [config.content] An array of content elements to append (after #text).
616  *  Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
617  *  Instances of OO.ui.Element will have their $element appended.
618  * @param {jQuery} [config.$content] Content elements to append (after #text).
619  * @param {jQuery} [config.$element] Wrapper element. Defaults to a new element with #getTagName.
620  * @param {any} [config.data] Custom data of any type or combination of types (e.g., string, number,
621  *  array, object).
622  *  Data can also be specified with the #setData method.
623  */
624 OO.ui.Element = function OoUiElement( config ) {
625         if ( OO.ui.isDemo ) {
626                 this.initialConfig = config;
627         }
628         // Configuration initialization
629         config = config || {};
631         // Properties
632         this.elementId = null;
633         this.visible = true;
634         this.data = config.data;
635         this.$element = config.$element ||
636                 $( window.document.createElement( this.getTagName() ) );
637         this.elementGroup = null;
639         // Initialization
640         const doc = OO.ui.Element.static.getDocument( this.$element );
641         if ( Array.isArray( config.classes ) ) {
642                 this.$element.addClass( config.classes );
643         }
644         if ( config.id ) {
645                 this.setElementId( config.id );
646         }
647         if ( config.text ) {
648                 this.$element.text( config.text );
649         }
650         if ( config.content ) {
651                 // The `content` property treats plain strings as text; use an
652                 // HtmlSnippet to append HTML content.  `OO.ui.Element`s get their
653                 // appropriate $element appended.
654                 this.$element.append( config.content.map( ( v ) => {
655                         if ( typeof v === 'string' ) {
656                                 // Escape string so it is properly represented in HTML.
657                                 // Don't create empty text nodes for empty strings.
658                                 return v ? doc.createTextNode( v ) : undefined;
659                         } else if ( v instanceof OO.ui.HtmlSnippet ) {
660                                 // Bypass escaping.
661                                 return v.toString();
662                         } else if ( v instanceof OO.ui.Element ) {
663                                 return v.$element;
664                         }
665                         return v;
666                 } ) );
667         }
668         if ( config.$content ) {
669                 // The `$content` property treats plain strings as HTML.
670                 this.$element.append( config.$content );
671         }
674 /* Setup */
676 OO.initClass( OO.ui.Element );
678 /* Static Properties */
681  * The name of the HTML tag used by the element.
683  * The static value may be ignored if the #getTagName method is overridden.
685  * @static
686  * @property {string}
687  */
688 OO.ui.Element.static.tagName = 'div';
690 /* Static Methods */
693  * Reconstitute a JavaScript object corresponding to a widget created
694  * by the PHP implementation.
696  * @param {HTMLElement|jQuery} node
697  *   A single node for the widget to infuse.
698  * @param {Object} [config] Configuration options
699  * @return {OO.ui.Element}
700  *   The `OO.ui.Element` corresponding to this (infusable) document node.
701  *   For `Tag` objects emitted on the HTML side (used occasionally for content)
702  *   the value returned is a newly-created Element wrapping around the existing
703  *   DOM node.
704  */
705 OO.ui.Element.static.infuse = function ( node, config ) {
706         const obj = OO.ui.Element.static.unsafeInfuse( node, config, false );
708         // Verify that the type matches up.
709         // FIXME: uncomment after T89721 is fixed, see T90929.
710         /*
711         if ( !( obj instanceof this['class'] ) ) {
712                 throw new Error( 'Infusion type mismatch!' );
713         }
714         */
715         return obj;
719  * Implementation helper for `infuse`; skips the type check and has an
720  * extra property so that only the top-level invocation touches the DOM.
722  * @private
723  * @param {HTMLElement|jQuery} elem
724  * @param {Object} [config] Configuration options
725  * @param {jQuery.Promise} [domPromise] A promise that will be resolved
726  *     when the top-level widget of this infusion is inserted into DOM,
727  *     replacing the original element; only used internally.
728  * @return {OO.ui.Element}
729  */
730 OO.ui.Element.static.unsafeInfuse = function ( elem, config, domPromise ) {
731         // look for a cached result of a previous infusion.
732         let $elem = $( elem );
734         if ( $elem.length > 1 ) {
735                 throw new Error( 'Collection contains more than one element' );
736         }
737         if ( !$elem.length ) {
738                 throw new Error( 'Widget not found' );
739         }
740         if ( $elem[ 0 ].$oouiInfused ) {
741                 $elem = $elem[ 0 ].$oouiInfused;
742         }
744         const id = $elem.attr( 'id' );
745         const doc = this.getDocument( $elem );
746         let data = $elem.data( 'ooui-infused' );
747         if ( data ) {
748                 // cached!
749                 if ( data === true ) {
750                         throw new Error( 'Circular dependency! ' + id );
751                 }
752                 if ( domPromise ) {
753                         // Pick up dynamic state, like focus, value of form inputs, scroll position, etc.
754                         const stateCache = data.constructor.static.gatherPreInfuseState( $elem, data );
755                         // Restore dynamic state after the new element is re-inserted into DOM under
756                         // infused parent.
757                         domPromise.done( data.restorePreInfuseState.bind( data, stateCache ) );
758                         const infusedChildrenCache = $elem.data( 'ooui-infused-children' );
759                         if ( infusedChildrenCache && infusedChildrenCache.length ) {
760                                 infusedChildrenCache.forEach( ( childData ) => {
761                                         const childState = childData.constructor.static.gatherPreInfuseState(
762                                                 $elem,
763                                                 childData
764                                         );
765                                         domPromise.done(
766                                                 childData.restorePreInfuseState.bind( childData, childState )
767                                         );
768                                 } );
769                         }
770                 }
771                 return data;
772         }
773         data = $elem.attr( 'data-ooui' );
774         if ( !data ) {
775                 throw new Error( 'No infusion data found: ' + id );
776         }
777         try {
778                 data = JSON.parse( data );
779         } catch ( _ ) {
780                 data = null;
781         }
782         if ( !( data && data._ ) ) {
783                 throw new Error( 'No valid infusion data found: ' + id );
784         }
785         if ( data._ === 'Tag' ) {
786                 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
787                 return new OO.ui.Element( Object.assign( {}, config, { $element: $elem } ) );
788         }
789         const parts = data._.split( '.' );
790         const cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
792         if ( !( cls && ( cls === OO.ui.Element || cls.prototype instanceof OO.ui.Element ) ) ) {
793                 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
794         }
796         let top;
797         if ( !domPromise ) {
798                 top = $.Deferred();
799                 domPromise = top.promise();
800         }
801         $elem.data( 'ooui-infused', true ); // prevent loops
802         data.id = id; // implicit
803         const infusedChildren = [];
804         data = OO.copy( data, null, ( value ) => {
805                 let infused;
806                 if ( OO.isPlainObject( value ) ) {
807                         if ( value.tag && doc.getElementById( value.tag ) ) {
808                                 infused = OO.ui.Element.static.unsafeInfuse(
809                                         doc.getElementById( value.tag ), config, domPromise
810                                 );
811                                 infusedChildren.push( infused );
812                                 // Flatten the structure
813                                 infusedChildren.push.apply(
814                                         infusedChildren,
815                                         infused.$element.data( 'ooui-infused-children' ) || []
816                                 );
817                                 infused.$element.removeData( 'ooui-infused-children' );
818                                 return infused;
819                         }
820                         if ( value.html !== undefined ) {
821                                 return new OO.ui.HtmlSnippet( value.html );
822                         }
823                 }
824         } );
825         // allow widgets to reuse parts of the DOM
826         data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data );
827         // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
828         const state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
829         // rebuild widget
830         // eslint-disable-next-line new-cap
831         const obj = new cls( Object.assign( {}, config, data ) );
832         // If anyone is holding a reference to the old DOM element,
833         // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
834         // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
835         $elem[ 0 ].$oouiInfused = obj.$element;
836         // now replace old DOM with this new DOM.
837         if ( top ) {
838                 // An efficient constructor might be able to reuse the entire DOM tree of the original
839                 // element, so only mutate the DOM if we need to.
840                 if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
841                         $elem.replaceWith( obj.$element );
842                 }
843                 top.resolve();
844         }
845         obj.$element
846                 .data( {
847                         'ooui-infused': obj,
848                         'ooui-infused-children': infusedChildren
849                 } )
850                 // set the 'data-ooui' attribute so we can identify infused widgets
851                 .attr( 'data-ooui', '' );
852         // restore dynamic state after the new element is inserted into DOM
853         domPromise.done( obj.restorePreInfuseState.bind( obj, state ) );
854         return obj;
858  * Pick out parts of `node`'s DOM to be reused when infusing a widget.
860  * This method **must not** make any changes to the DOM, only find interesting pieces and add them
861  * to `config` (which should then be returned). Actual DOM juggling should then be done by the
862  * constructor, which will be given the enhanced config.
864  * @protected
865  * @param {HTMLElement} node
866  * @param {Object} config
867  * @return {Object}
868  */
869 OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) {
870         return config;
874  * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM
875  * node (and its children) that represent an Element of the same class and the given configuration,
876  * generated by the PHP implementation.
878  * This method is called just before `node` is detached from the DOM. The return value of this
879  * function will be passed to #restorePreInfuseState after the newly created widget's #$element
880  * is inserted into DOM to replace `node`.
882  * @protected
883  * @param {HTMLElement} node
884  * @param {Object} config
885  * @return {Object}
886  */
887 OO.ui.Element.static.gatherPreInfuseState = function () {
888         return {};
892  * Get the document of an element.
894  * @static
895  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
896  * @return {HTMLDocument|null} Document object
897  */
898 OO.ui.Element.static.getDocument = function ( obj ) {
899         // HTMLElement
900         return obj.ownerDocument ||
901                 // Window
902                 obj.document ||
903                 // HTMLDocument
904                 ( obj.nodeType === Node.DOCUMENT_NODE && obj ) ||
905                 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
906                 ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
907                 // Empty jQuery selections might have a context
908                 obj.context ||
909                 null;
913  * Get the window of an element or document.
915  * @static
916  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
917  * @return {Window} Window object
918  */
919 OO.ui.Element.static.getWindow = function ( obj ) {
920         const doc = this.getDocument( obj );
921         return doc.defaultView;
925  * Get the direction of an element or document.
927  * @static
928  * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
929  * @return {string} Text direction, either 'ltr' or 'rtl'
930  */
931 OO.ui.Element.static.getDir = function ( obj ) {
932         if ( obj instanceof $ ) {
933                 obj = obj[ 0 ];
934         }
935         const isDoc = obj.nodeType === Node.DOCUMENT_NODE;
936         const isWin = obj.document !== undefined;
937         if ( isDoc || isWin ) {
938                 if ( isWin ) {
939                         obj = obj.document;
940                 }
941                 obj = obj.body;
942         }
943         return $( obj ).css( 'direction' );
947  * Get the offset between two frames.
949  * TODO: Make this function not use recursion.
951  * @static
952  * @param {Window} from Window of the child frame
953  * @param {Window} [to=window] Window of the parent frame
954  * @param {Object} [offset] Offset to start with, used internally
955  * @return {Object} Offset object, containing left and top properties
956  */
957 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
958         if ( !to ) {
959                 to = window;
960         }
961         if ( !offset ) {
962                 offset = { top: 0, left: 0 };
963         }
964         if ( from.parent === from ) {
965                 return offset;
966         }
968         // Get iframe element
969         let frame;
970         const frames = from.parent.document.getElementsByTagName( 'iframe' );
971         for ( let i = 0, len = frames.length; i < len; i++ ) {
972                 if ( frames[ i ].contentWindow === from ) {
973                         frame = frames[ i ];
974                         break;
975                 }
976         }
978         // Recursively accumulate offset values
979         if ( frame ) {
980                 const rect = frame.getBoundingClientRect();
981                 offset.left += rect.left;
982                 offset.top += rect.top;
983                 if ( from !== to ) {
984                         this.getFrameOffset( from.parent, offset );
985                 }
986         }
987         return offset;
991  * Get the offset between two elements.
993  * The two elements may be in a different frame, but in that case the frame $element is in must
994  * be contained in the frame $anchor is in.
996  * @static
997  * @param {jQuery} $element Element whose position to get
998  * @param {jQuery} $anchor Element to get $element's position relative to
999  * @return {Object} Translated position coordinates, containing top and left properties
1000  */
1001 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
1002         const pos = $element.offset();
1003         const anchorPos = $anchor.offset();
1004         const anchorDocument = this.getDocument( $anchor );
1006         let elementDocument = this.getDocument( $element );
1008         // If $element isn't in the same document as $anchor, traverse up
1009         while ( elementDocument !== anchorDocument ) {
1010                 const iframe = elementDocument.defaultView.frameElement;
1011                 if ( !iframe ) {
1012                         throw new Error( '$element frame is not contained in $anchor frame' );
1013                 }
1014                 const iframePos = $( iframe ).offset();
1015                 pos.left += iframePos.left;
1016                 pos.top += iframePos.top;
1017                 elementDocument = this.getDocument( iframe );
1018         }
1019         pos.left -= anchorPos.left;
1020         pos.top -= anchorPos.top;
1021         return pos;
1025  * Get element border sizes.
1027  * @static
1028  * @param {HTMLElement} el Element to measure
1029  * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1030  */
1031 OO.ui.Element.static.getBorders = function ( el ) {
1032         const doc = this.getDocument( el ),
1033                 win = doc.defaultView,
1034                 style = win.getComputedStyle( el, null ),
1035                 $el = $( el ),
1036                 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1037                 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1038                 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1039                 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1041         return {
1042                 top: top,
1043                 left: left,
1044                 bottom: bottom,
1045                 right: right
1046         };
1050  * Get dimensions of an element or window.
1052  * @static
1053  * @param {HTMLElement|Window} el Element to measure
1054  * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1055  */
1056 OO.ui.Element.static.getDimensions = function ( el ) {
1057         const doc = this.getDocument( el ),
1058                 win = doc.defaultView;
1060         if ( win === el || el === doc.documentElement ) {
1061                 const $win = $( win );
1062                 return {
1063                         borders: { top: 0, left: 0, bottom: 0, right: 0 },
1064                         scroll: {
1065                                 top: $win.scrollTop(),
1066                                 left: OO.ui.Element.static.getScrollLeft( win )
1067                         },
1068                         scrollbar: { right: 0, bottom: 0 },
1069                         rect: {
1070                                 top: 0,
1071                                 left: 0,
1072                                 bottom: $win.innerHeight(),
1073                                 right: $win.innerWidth()
1074                         }
1075                 };
1076         } else {
1077                 const $el = $( el );
1078                 return {
1079                         borders: this.getBorders( el ),
1080                         scroll: {
1081                                 top: $el.scrollTop(),
1082                                 left: OO.ui.Element.static.getScrollLeft( el )
1083                         },
1084                         scrollbar: {
1085                                 right: $el.innerWidth() - el.clientWidth,
1086                                 bottom: $el.innerHeight() - el.clientHeight
1087                         },
1088                         rect: el.getBoundingClientRect()
1089                 };
1090         }
1093 ( function () {
1094         let rtlScrollType = null;
1096         // Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1097         // Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1098         function rtlScrollTypeTest() {
1099                 const $definer = $( '<div>' ).attr( {
1100                                 dir: 'rtl',
1101                                 style: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
1102                         } ).text( 'ABCD' ),
1103                         definer = $definer[ 0 ];
1105                 $definer.appendTo( 'body' );
1106                 if ( definer.scrollLeft > 0 ) {
1107                         // Safari, Chrome
1108                         rtlScrollType = 'default';
1109                 } else {
1110                         definer.scrollLeft = 1;
1111                         if ( definer.scrollLeft === 0 ) {
1112                                 // Firefox, old Opera
1113                                 rtlScrollType = 'negative';
1114                         }
1115                 }
1116                 $definer.remove();
1117         }
1119         function isRoot( el ) {
1120                 return el === el.window ||
1121                         el === el.ownerDocument.body ||
1122                         el === el.ownerDocument.documentElement;
1123         }
1125         /**
1126          * Convert native `scrollLeft` value to a value consistent between browsers. See #getScrollLeft.
1127          *
1128          * @param {number} nativeOffset Native `scrollLeft` value
1129          * @param {HTMLElement|Window} el Element from which the value was obtained
1130          * @return {number}
1131          */
1132         OO.ui.Element.static.computeNormalizedScrollLeft = function ( nativeOffset, el ) {
1133                 // All browsers use the correct scroll type ('negative') on the root, so don't
1134                 // do any fixups when looking at the root element
1135                 const direction = isRoot( el ) ? 'ltr' : $( el ).css( 'direction' );
1137                 if ( direction === 'rtl' ) {
1138                         if ( rtlScrollType === null ) {
1139                                 rtlScrollTypeTest();
1140                         }
1141                         if ( rtlScrollType === 'reverse' ) {
1142                                 return -nativeOffset;
1143                         } else if ( rtlScrollType === 'default' ) {
1144                                 return nativeOffset - el.scrollWidth + el.clientWidth;
1145                         }
1146                 }
1148                 return nativeOffset;
1149         };
1151         /**
1152          * Convert our normalized `scrollLeft` value to a value for current browser. See #getScrollLeft.
1153          *
1154          * @param {number} normalizedOffset Normalized `scrollLeft` value
1155          * @param {HTMLElement|Window} el Element on which the value will be set
1156          * @return {number}
1157          */
1158         OO.ui.Element.static.computeNativeScrollLeft = function ( normalizedOffset, el ) {
1159                 // All browsers use the correct scroll type ('negative') on the root, so don't
1160                 // do any fixups when looking at the root element
1161                 const direction = isRoot( el ) ? 'ltr' : $( el ).css( 'direction' );
1163                 if ( direction === 'rtl' ) {
1164                         if ( rtlScrollType === null ) {
1165                                 rtlScrollTypeTest();
1166                         }
1167                         if ( rtlScrollType === 'reverse' ) {
1168                                 return -normalizedOffset;
1169                         } else if ( rtlScrollType === 'default' ) {
1170                                 return normalizedOffset + el.scrollWidth - el.clientWidth;
1171                         }
1172                 }
1174                 return normalizedOffset;
1175         };
1177         /**
1178          * Get the number of pixels that an element's content is scrolled to the left.
1179          *
1180          * This function smooths out browser inconsistencies (nicely described in the README at
1181          * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1182          * with Firefox's 'scrollLeft', which seems the most sensible.
1183          *
1184          * (Firefox's scrollLeft handling is nice because it increases from left to right, consistently
1185          * with `getBoundingClientRect().left` and related APIs; because initial value is zero, so
1186          * resetting it is easy; because adapting a hardcoded scroll position to a symmetrical RTL
1187          * interface requires just negating it, rather than involving `clientWidth` and `scrollWidth`;
1188          * and because if you mess up and don't adapt your code to RTL, it will scroll to the beginning
1189          * rather than somewhere randomly in the middle but not where you wanted.)
1190          *
1191          * @static
1192          * @method
1193          * @param {HTMLElement|Window} el Element to measure
1194          * @return {number} Scroll position from the left.
1195          *  If the element's direction is LTR, this is a positive number between `0` (initial scroll
1196          *  position) and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1197          *  If the element's direction is RTL, this is a negative number between `0` (initial scroll
1198          *  position) and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1199          */
1200         OO.ui.Element.static.getScrollLeft = function ( el ) {
1201                 let scrollLeft = isRoot( el ) ? $( window ).scrollLeft() : el.scrollLeft;
1202                 scrollLeft = OO.ui.Element.static.computeNormalizedScrollLeft( scrollLeft, el );
1203                 return scrollLeft;
1204         };
1206         /**
1207          * Set the number of pixels that an element's content is scrolled to the left.
1208          *
1209          * See #getScrollLeft.
1210          *
1211          * @static
1212          * @method
1213          * @param {HTMLElement|Window} el Element to scroll (and to use in calculations)
1214          * @param {number} scrollLeft Scroll position from the left.
1215          *  If the element's direction is LTR, this must be a positive number between
1216          *  `0` (initial scroll position) and `el.scrollWidth - el.clientWidth`
1217          *  (furthest possible scroll position).
1218          *  If the element's direction is RTL, this must be a negative number between
1219          *  `0` (initial scroll position) and `-el.scrollWidth + el.clientWidth`
1220          *  (furthest possible scroll position).
1221          */
1222         OO.ui.Element.static.setScrollLeft = function ( el, scrollLeft ) {
1223                 scrollLeft = OO.ui.Element.static.computeNativeScrollLeft( scrollLeft, el );
1224                 if ( isRoot( el ) ) {
1225                         $( window ).scrollLeft( scrollLeft );
1226                 } else {
1227                         el.scrollLeft = scrollLeft;
1228                 }
1229         };
1230 }() );
1233  * Get the root scrollable element of given element's document.
1235  * Support: Chrome <= 60
1236  * On older versions of Blink, `document.documentElement` can't be used to get or set
1237  * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1238  * lets us use 'body' or 'documentElement' based on what is working.
1240  * https://code.google.com/p/chromium/issues/detail?id=303131
1242  * @static
1243  * @param {HTMLElement} el Element to find root scrollable parent for
1244  * @return {HTMLBodyElement|HTMLHtmlElement} Scrollable parent, `<body>` or `<html>`
1245  */
1246 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1247         const doc = this.getDocument( el );
1249         if ( OO.ui.scrollableElement === undefined ) {
1250                 const body = doc.body;
1251                 const scrollTop = body.scrollTop;
1252                 body.scrollTop = 1;
1254                 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1255                 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1256                 if ( Math.round( body.scrollTop ) === 1 ) {
1257                         body.scrollTop = scrollTop;
1258                         OO.ui.scrollableElement = 'body';
1259                 } else {
1260                         OO.ui.scrollableElement = 'documentElement';
1261                 }
1262         }
1264         return doc[ OO.ui.scrollableElement ];
1268  * Get closest scrollable container.
1270  * Traverses up until either a scrollable element or the root is reached, in which case the root
1271  * scrollable element will be returned (see #getRootScrollableElement).
1273  * @static
1274  * @param {HTMLElement} el Element to find scrollable container for
1275  * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1276  * @return {HTMLElement} Closest scrollable container
1277  */
1278 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1279         const doc = this.getDocument( el );
1280         const rootScrollableElement = this.getRootScrollableElement( el );
1281         // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1282         // 'overflow-y' have different values, so we need to check the separate properties.
1283         let props = [ 'overflow-x', 'overflow-y' ];
1284         let $parent = $( el ).parent();
1286         if ( el === doc.documentElement ) {
1287                 return rootScrollableElement;
1288         }
1290         if ( dimension === 'x' || dimension === 'y' ) {
1291                 props = [ 'overflow-' + dimension ];
1292         }
1294         // The parent of <html> is the document, so check we haven't traversed that far
1295         while ( $parent.length && $parent[ 0 ] !== doc ) {
1296                 if ( $parent[ 0 ] === rootScrollableElement ) {
1297                         return $parent[ 0 ];
1298                 }
1299                 let i = props.length;
1300                 while ( i-- ) {
1301                         const val = $parent.css( props[ i ] );
1302                         // We assume that elements with 'overflow' (in any direction) set to 'hidden' will
1303                         // never be scrolled in that direction, but they can actually be scrolled
1304                         // programatically. The user can unintentionally perform a scroll in such case even if
1305                         // the application doesn't scroll programatically, e.g. when jumping to an anchor, or
1306                         // when using built-in find functionality.
1307                         // This could cause funny issues...
1308                         if ( val === 'auto' || val === 'scroll' ) {
1309                                 if ( $parent[ 0 ] === doc.body ) {
1310                                         // If overflow is set on <body>, return the rootScrollableElement
1311                                         // (<body> or <html>) as <body> may not be scrollable.
1312                                         return rootScrollableElement;
1313                                 } else {
1314                                         return $parent[ 0 ];
1315                                 }
1316                         }
1317                 }
1318                 $parent = $parent.parent();
1319         }
1320         // The element is unattached… return something moderately sensible.
1321         return rootScrollableElement;
1325  * Scroll element into view.
1327  * @static
1328  * @param {HTMLElement|Object} elOrPosition Element to scroll into view
1329  * @param {Object} [config] Configuration options
1330  * @param {string} [config.animate=true] Animate to the new scroll offset.
1331  * @param {string} [config.duration='fast'] jQuery animation duration value
1332  * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1333  *  to scroll in both directions
1334  * @param {Object} [config.alignToTop=false] Aligns the top of the element to the top of the visible
1335  *  area of the scrollable ancestor.
1336  * @param {Object} [config.padding] Additional padding on the container to scroll past.
1337  *  Object containing any of 'top', 'bottom', 'left', or 'right' as numbers.
1338  * @param {Object} [config.scrollContainer] Scroll container. Defaults to
1339  *  getClosestScrollableContainer of the element.
1340  * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1341  */
1342 OO.ui.Element.static.scrollIntoView = function ( elOrPosition, config ) {
1343         const deferred = $.Deferred();
1345         // Configuration initialization
1346         config = config || {};
1348         const padding = Object.assign( {
1349                 top: 0,
1350                 bottom: 0,
1351                 left: 0,
1352                 right: 0
1353         }, config.padding );
1355         let animate = config.animate !== false;
1356         if ( window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ) {
1357                 // Respect 'prefers-reduced-motion' user preference
1358                 animate = false;
1359         }
1361         const animations = {};
1362         const elementPosition = elOrPosition instanceof HTMLElement ?
1363                 this.getDimensions( elOrPosition ).rect :
1364                 elOrPosition;
1365         const container = config.scrollContainer || (
1366                 elOrPosition instanceof HTMLElement ?
1367                         this.getClosestScrollableContainer( elOrPosition, config.direction ) :
1368                         // No scrollContainer or element, use global document
1369                         this.getClosestScrollableContainer( window.document.body )
1370         );
1371         const $container = $( container );
1372         const containerDimensions = this.getDimensions( container );
1373         const $window = $( this.getWindow( container ) );
1375         // Compute the element's position relative to the container
1376         let position;
1377         if ( $container.is( 'html, body' ) ) {
1378                 // If the scrollable container is the root, this is easy
1379                 position = {
1380                         top: elementPosition.top,
1381                         bottom: $window.innerHeight() - elementPosition.bottom,
1382                         left: elementPosition.left,
1383                         right: $window.innerWidth() - elementPosition.right
1384                 };
1385         } else {
1386                 // Otherwise, we have to subtract el's coordinates from container's coordinates
1387                 position = {
1388                         top: elementPosition.top -
1389                                 ( containerDimensions.rect.top + containerDimensions.borders.top ),
1390                         bottom: containerDimensions.rect.bottom - containerDimensions.borders.bottom -
1391                                 containerDimensions.scrollbar.bottom - elementPosition.bottom,
1392                         left: elementPosition.left -
1393                                 ( containerDimensions.rect.left + containerDimensions.borders.left ),
1394                         right: containerDimensions.rect.right - containerDimensions.borders.right -
1395                                 containerDimensions.scrollbar.right - elementPosition.right
1396                 };
1397         }
1399         if ( !config.direction || config.direction === 'y' ) {
1400                 if ( position.top < padding.top || config.alignToTop ) {
1401                         animations.scrollTop = containerDimensions.scroll.top + position.top - padding.top;
1402                 } else if ( position.bottom < padding.bottom ) {
1403                         animations.scrollTop = containerDimensions.scroll.top +
1404                                 // Scroll the bottom into view, but not at the expense
1405                                 // of scrolling the top out of view
1406                                 Math.min( position.top - padding.top, -position.bottom + padding.bottom );
1407                 }
1408         }
1409         if ( !config.direction || config.direction === 'x' ) {
1410                 if ( position.left < padding.left ) {
1411                         animations.scrollLeft = containerDimensions.scroll.left + position.left - padding.left;
1412                 } else if ( position.right < padding.right ) {
1413                         animations.scrollLeft = containerDimensions.scroll.left +
1414                                 // Scroll the right into view, but not at the expense
1415                                 // of scrolling the left out of view
1416                                 Math.min( position.left - padding.left, -position.right + padding.right );
1417                 }
1418                 if ( animations.scrollLeft !== undefined ) {
1419                         animations.scrollLeft =
1420                                 OO.ui.Element.static.computeNativeScrollLeft( animations.scrollLeft, container );
1421                 }
1422         }
1423         if ( !$.isEmptyObject( animations ) ) {
1424                 if ( animate ) {
1425                         // eslint-disable-next-line no-jquery/no-animate
1426                         $container.stop( true ).animate( animations, {
1427                                 duration: config.duration === undefined ? 'fast' : config.duration,
1428                                 always: deferred.resolve
1429                         } );
1430                 } else {
1431                         $container.stop( true );
1432                         for ( const method in animations ) {
1433                                 $container[ method ]( animations[ method ] );
1434                         }
1435                         deferred.resolve();
1436                 }
1437         } else {
1438                 deferred.resolve();
1439         }
1440         return deferred.promise();
1444  * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1445  * and reserve space for them, because it probably doesn't.
1447  * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1448  * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1449  * to first actually detach (or hide, but detaching is simpler) all children, *then* force a
1450  * reflow, and then reattach (or show) them back.
1452  * @static
1453  * @param {HTMLElement} el Element to reconsider the scrollbars on
1454  */
1455 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1456         // Save scroll position
1457         const scrollLeft = el.scrollLeft;
1458         const scrollTop = el.scrollTop;
1459         const nodes = [];
1460         // Detach all children
1461         while ( el.firstChild ) {
1462                 nodes.push( el.firstChild );
1463                 el.removeChild( el.firstChild );
1464         }
1465         // Force reflow
1466         // eslint-disable-next-line no-unused-expressions
1467         el.offsetHeight;
1468         // Reattach all children
1469         for ( let i = 0, len = nodes.length; i < len; i++ ) {
1470                 el.appendChild( nodes[ i ] );
1471         }
1472         // Restore scroll position (no-op if scrollbars disappeared)
1473         el.scrollLeft = scrollLeft;
1474         el.scrollTop = scrollTop;
1477 /* Methods */
1480  * Toggle visibility of an element.
1482  * @param {boolean} [show] Make element visible, omit to toggle visibility
1483  * @fires OO.ui.Widget#toggle
1484  * @chainable
1485  * @return {OO.ui.Element} The element, for chaining
1486  */
1487 OO.ui.Element.prototype.toggle = function ( show ) {
1488         show = show === undefined ? !this.visible : !!show;
1490         if ( show !== this.isVisible() ) {
1491                 this.visible = show;
1492                 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1493                 this.emit( 'toggle', show );
1494         }
1496         return this;
1500  * Check if element is visible.
1502  * @return {boolean} element is visible
1503  */
1504 OO.ui.Element.prototype.isVisible = function () {
1505         return this.visible;
1509  * Get element data.
1511  * @return {any} Element data
1512  */
1513 OO.ui.Element.prototype.getData = function () {
1514         return this.data;
1518  * Set element data.
1520  * @param {any} data Element data
1521  * @chainable
1522  * @return {OO.ui.Element} The element, for chaining
1523  */
1524 OO.ui.Element.prototype.setData = function ( data ) {
1525         this.data = data;
1526         return this;
1530  * Set the element has an 'id' attribute.
1532  * @param {string} id
1533  * @chainable
1534  * @return {OO.ui.Element} The element, for chaining
1535  */
1536 OO.ui.Element.prototype.setElementId = function ( id ) {
1537         this.elementId = id;
1538         this.$element.attr( 'id', id );
1539         return this;
1543  * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1544  * and return its value.
1546  * @return {string}
1547  */
1548 OO.ui.Element.prototype.getElementId = function () {
1549         if ( this.elementId === null ) {
1550                 this.setElementId( OO.ui.generateElementId() );
1551         }
1552         return this.elementId;
1556  * Check if element supports one or more methods.
1558  * @param {string|string[]} methods Method or list of methods to check
1559  * @return {boolean} All methods are supported
1560  */
1561 OO.ui.Element.prototype.supports = function ( methods ) {
1562         if ( !Array.isArray( methods ) ) {
1563                 return typeof this[ methods ] === 'function';
1564         }
1566         return methods.every( ( method ) => typeof this[ method ] === 'function' );
1570  * Update the theme-provided classes.
1572  * This is called in element mixins and widget classes any time state changes.
1573  *   Updating is debounced, minimizing overhead of changing multiple attributes and
1574  *   guaranteeing that theme updates do not occur within an element's constructor
1575  */
1576 OO.ui.Element.prototype.updateThemeClasses = function () {
1577         OO.ui.theme.queueUpdateElementClasses( this );
1581  * Get the HTML tag name.
1583  * Override this method to base the result on instance information.
1585  * @return {string} HTML tag name
1586  */
1587 OO.ui.Element.prototype.getTagName = function () {
1588         return this.constructor.static.tagName;
1592  * Check if the element is attached to the DOM
1594  * @return {boolean} The element is attached to the DOM
1595  */
1596 OO.ui.Element.prototype.isElementAttached = function () {
1597         return this.$element[ 0 ].isConnected;
1601  * Get the DOM document.
1603  * @return {HTMLDocument} Document object
1604  */
1605 OO.ui.Element.prototype.getElementDocument = function () {
1606         // Don't cache this in other ways either because subclasses could can change this.$element
1607         return OO.ui.Element.static.getDocument( this.$element );
1611  * Get the DOM window.
1613  * @return {Window} Window object
1614  */
1615 OO.ui.Element.prototype.getElementWindow = function () {
1616         return OO.ui.Element.static.getWindow( this.$element );
1620  * Get closest scrollable container.
1622  * @return {HTMLElement} Closest scrollable container
1623  */
1624 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1625         return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1629  * Get group element is in.
1631  * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1632  */
1633 OO.ui.Element.prototype.getElementGroup = function () {
1634         return this.elementGroup;
1638  * Set group element is in.
1640  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1641  * @chainable
1642  * @return {OO.ui.Element} The element, for chaining
1643  */
1644 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1645         this.elementGroup = group;
1646         return this;
1650  * Scroll element into view.
1652  * @param {Object} [config] Configuration options
1653  * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1654  */
1655 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1656         if (
1657                 !this.isElementAttached() ||
1658                 !this.isVisible() ||
1659                 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1660         ) {
1661                 return $.Deferred().resolve();
1662         }
1663         return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1667  * Restore the pre-infusion dynamic state for this widget.
1669  * This method is called after #$element has been inserted into DOM. The parameter is the return
1670  * value of #gatherPreInfuseState.
1672  * @protected
1673  * @param {Object} state
1674  */
1675 OO.ui.Element.prototype.restorePreInfuseState = function () {
1679  * Wraps an HTML snippet for use with configuration values which default
1680  * to strings.  This bypasses the default html-escaping done to string
1681  * values.
1683  * @class
1685  * @constructor
1686  * @param {string} content HTML content
1687  */
1688 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
1689         // Properties
1690         this.content = content;
1693 /* Setup */
1695 OO.initClass( OO.ui.HtmlSnippet );
1697 /* Methods */
1700  * Render into HTML.
1702  * @return {string} Unchanged HTML snippet.
1703  */
1704 OO.ui.HtmlSnippet.prototype.toString = function () {
1705         return this.content;
1709  * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in
1710  * a way that is centrally controlled and can be updated dynamically. Layouts can be, and usually
1711  * are, combined.
1712  * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout},
1713  * {@link OO.ui.FormLayout FormLayout}, {@link OO.ui.PanelLayout PanelLayout},
1714  * {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1715  * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout}
1716  * for more information and examples.
1718  * @abstract
1719  * @class
1720  * @extends OO.ui.Element
1721  * @mixes OO.EventEmitter
1723  * @constructor
1724  * @param {Object} [config] Configuration options
1725  */
1726 OO.ui.Layout = function OoUiLayout( config ) {
1727         // Configuration initialization
1728         config = config || {};
1730         // Parent constructor
1731         OO.ui.Layout.super.call( this, config );
1733         // Mixin constructors
1734         OO.EventEmitter.call( this );
1736         // Initialization
1737         this.$element.addClass( 'oo-ui-layout' );
1740 /* Setup */
1742 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1743 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1745 /* Methods */
1748  * Reset scroll offsets
1750  * @chainable
1751  * @return {OO.ui.Layout} The layout, for chaining
1752  */
1753 OO.ui.Layout.prototype.resetScroll = function () {
1754         this.$element[ 0 ].scrollTop = 0;
1755         OO.ui.Element.static.setScrollLeft( this.$element[ 0 ], 0 );
1757         return this;
1761  * Widgets are compositions of one or more OOUI elements that users can both view
1762  * and interact with. All widgets can be configured and modified via a standard API,
1763  * and their state can change dynamically according to a model.
1765  * @abstract
1766  * @class
1767  * @extends OO.ui.Element
1768  * @mixes OO.EventEmitter
1770  * @constructor
1771  * @param {Object} [config] Configuration options
1772  * @param {boolean} [config.disabled=false] Disable the widget. Disabled widgets cannot be used and their
1773  *  appearance reflects this state.
1774  */
1775 OO.ui.Widget = function OoUiWidget( config ) {
1776         // Parent constructor
1777         OO.ui.Widget.super.call( this, config );
1779         // Mixin constructors
1780         OO.EventEmitter.call( this );
1782         // Properties
1783         this.disabled = null;
1784         this.wasDisabled = null;
1786         // Initialization
1787         this.$element.addClass( 'oo-ui-widget' );
1788         this.setDisabled( config && config.disabled );
1791 /* Setup */
1793 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1794 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1796 /* Events */
1799  * A 'disable' event is emitted when the disabled state of the widget changes
1800  * (i.e. on disable **and** enable).
1802  * @event OO.ui.Widget#disable
1803  * @param {boolean} disabled Widget is disabled
1804  */
1807  * A 'toggle' event is emitted when the visibility of the widget changes.
1809  * @event OO.ui.Widget#toggle
1810  * @param {boolean} visible Widget is visible
1811  */
1813 /* Methods */
1816  * Check if the widget is disabled.
1818  * @return {boolean} Widget is disabled
1819  */
1820 OO.ui.Widget.prototype.isDisabled = function () {
1821         return this.disabled;
1825  * Set the 'disabled' state of the widget.
1827  * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1829  * @param {boolean} [disabled=false] Disable widget
1830  * @chainable
1831  * @return {OO.ui.Widget} The widget, for chaining
1832  */
1833 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1834         this.disabled = !!disabled;
1835         const isDisabled = this.isDisabled();
1836         if ( isDisabled !== this.wasDisabled ) {
1837                 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1838                 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1839                 this.$element.attr( 'aria-disabled', isDisabled ? 'true' : null );
1840                 this.emit( 'disable', isDisabled );
1841                 this.updateThemeClasses();
1842                 this.wasDisabled = isDisabled;
1843         }
1845         return this;
1849  * Update the disabled state, in case of changes in parent widget.
1851  * @chainable
1852  * @return {OO.ui.Widget} The widget, for chaining
1853  */
1854 OO.ui.Widget.prototype.updateDisabled = function () {
1855         this.setDisabled( this.disabled );
1856         return this;
1860  * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1861  * value.
1863  * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1864  * instead.
1866  * @return {string|null} The ID of the labelable element
1867  */
1868 OO.ui.Widget.prototype.getInputId = function () {
1869         return null;
1873  * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1874  * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1875  * override this method to provide intuitive, accessible behavior.
1877  * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1878  * Individual widgets may override it too.
1880  * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1881  * directly.
1882  */
1883 OO.ui.Widget.prototype.simulateLabelClick = function () {
1887  * Set the element with the given ID as a label for this widget.
1889  * @param {string|null} id
1890  */
1891 OO.ui.Widget.prototype.setLabelledBy = function ( id ) {
1892         if ( id ) {
1893                 this.$element.attr( 'aria-labelledby', id );
1894         } else {
1895                 this.$element.removeAttr( 'aria-labelledby' );
1896         }
1900  * Theme logic.
1902  * @abstract
1903  * @class
1905  * @constructor
1906  */
1907 OO.ui.Theme = function OoUiTheme() {
1908         this.elementClassesQueue = [];
1909         this.debouncedUpdateQueuedElementClasses = OO.ui.debounce( this.updateQueuedElementClasses );
1912 /* Setup */
1914 OO.initClass( OO.ui.Theme );
1916 /* Methods */
1919  * Get a list of classes to be applied to a widget.
1921  * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1922  * otherwise state transitions will not work properly.
1924  * @param {OO.ui.Element} element Element for which to get classes
1925  * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1926  */
1927 OO.ui.Theme.prototype.getElementClasses = function () {
1928         return { on: [], off: [] };
1932  * Update CSS classes provided by the theme.
1934  * For elements with theme logic hooks, this should be called any time there's a state change.
1936  * @param {OO.ui.Element} element Element for which to update classes
1937  */
1938 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
1939         const domElements = [];
1941         if ( element.$icon ) {
1942                 domElements.push( element.$icon[ 0 ] );
1943         }
1944         if ( element.$indicator ) {
1945                 domElements.push( element.$indicator[ 0 ] );
1946         }
1948         if ( domElements.length ) {
1949                 const classes = this.getElementClasses( element );
1950                 $( domElements )
1951                         .removeClass( classes.off )
1952                         .addClass( classes.on );
1953         }
1957  * @private
1958  */
1959 OO.ui.Theme.prototype.updateQueuedElementClasses = function () {
1960         for ( let i = 0; i < this.elementClassesQueue.length; i++ ) {
1961                 this.updateElementClasses( this.elementClassesQueue[ i ] );
1962         }
1963         // Clear the queue
1964         this.elementClassesQueue = [];
1968  * Queue #updateElementClasses to be called for this element.
1970  * QUnit tests override this method to directly call #queueUpdateElementClasses,
1971  *   to make them synchronous.
1973  * @param {OO.ui.Element} element Element for which to update classes
1974  */
1975 OO.ui.Theme.prototype.queueUpdateElementClasses = function ( element ) {
1976         // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1977         // the most common case (this method is often called repeatedly for the same element).
1978         if ( this.elementClassesQueue.lastIndexOf( element ) !== -1 ) {
1979                 return;
1980         }
1981         this.elementClassesQueue.push( element );
1982         this.debouncedUpdateQueuedElementClasses();
1986  * Get the transition duration in milliseconds for dialogs opening/closing
1988  * The dialog should be fully rendered this many milliseconds after the
1989  * ready process has executed.
1991  * @return {number} Transition duration in milliseconds
1992  */
1993 OO.ui.Theme.prototype.getDialogTransitionDuration = function () {
1994         return 0;
1998  * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1999  * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
2000  * order in which users will navigate through the focusable elements via the Tab key.
2002  *     @example
2003  *     // TabIndexedElement is mixed into the ButtonWidget class
2004  *     // to provide a tabIndex property.
2005  *     const button1 = new OO.ui.ButtonWidget( {
2006  *             label: 'fourth',
2007  *             tabIndex: 4
2008  *         } ),
2009  *         button2 = new OO.ui.ButtonWidget( {
2010  *             label: 'second',
2011  *             tabIndex: 2
2012  *         } ),
2013  *         button3 = new OO.ui.ButtonWidget( {
2014  *             label: 'third',
2015  *             tabIndex: 3
2016  *         } ),
2017  *         button4 = new OO.ui.ButtonWidget( {
2018  *             label: 'first',
2019  *             tabIndex: 1
2020  *         } );
2021  *     $( document.body ).append(
2022  *         button1.$element,
2023  *         button2.$element,
2024  *         button3.$element,
2025  *         button4.$element
2026  *      );
2028  * @abstract
2029  * @class
2031  * @constructor
2032  * @param {Object} [config] Configuration options
2033  * @param {jQuery} [config.$tabIndexed] The element that should use the tabindex functionality. By default,
2034  *  the functionality is applied to the element created by the class ($element). If a different
2035  *  element is specified, the tabindex functionality will be applied to it instead.
2036  * @param {string|number|null} [config.tabIndex=0] Number that specifies the element’s position in the
2037  *  tab-navigation order (e.g., 1 for the first focusable element). Use 0 to use the default
2038  *  navigation order; use -1 to remove the element from the tab-navigation flow.
2039  */
2040 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
2041         // Configuration initialization
2042         config = Object.assign( { tabIndex: 0 }, config );
2044         // Properties
2045         this.$tabIndexed = null;
2046         this.tabIndex = null;
2048         // Events
2049         this.connect( this, {
2050                 disable: 'onTabIndexedElementDisable'
2051         } );
2053         // Initialization
2054         this.setTabIndex( config.tabIndex );
2055         this.setTabIndexedElement( config.$tabIndexed || this.$element );
2058 /* Setup */
2060 OO.initClass( OO.ui.mixin.TabIndexedElement );
2062 /* Methods */
2065  * Set the element that should use the tabindex functionality.
2067  * This method is used to retarget a tabindex mixin so that its functionality applies
2068  * to the specified element. If an element is currently using the functionality, the mixin’s
2069  * effect on that element is removed before the new element is set up.
2071  * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
2072  * @chainable
2073  * @return {OO.ui.Element} The element, for chaining
2074  */
2075 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
2076         const tabIndex = this.tabIndex;
2077         // Remove attributes from old $tabIndexed
2078         this.setTabIndex( null );
2079         // Force update of new $tabIndexed
2080         this.$tabIndexed = $tabIndexed;
2081         this.tabIndex = tabIndex;
2082         return this.updateTabIndex();
2086  * Set the value of the tabindex.
2088  * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
2089  * @chainable
2090  * @return {OO.ui.Element} The element, for chaining
2091  */
2092 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
2093         tabIndex = /^-?\d+$/.test( tabIndex ) ? Number( tabIndex ) : null;
2095         if ( this.tabIndex !== tabIndex ) {
2096                 this.tabIndex = tabIndex;
2097                 this.updateTabIndex();
2098         }
2100         return this;
2104  * Update the `tabindex` attribute, in case of changes to tab index or
2105  * disabled state.
2107  * @private
2108  * @chainable
2109  * @return {OO.ui.Element} The element, for chaining
2110  */
2111 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
2112         if ( this.$tabIndexed ) {
2113                 if ( this.tabIndex !== null ) {
2114                         // Do not index over disabled elements
2115                         this.$tabIndexed.attr( {
2116                                 tabindex: this.isDisabled() ? -1 : this.tabIndex,
2117                                 // Support: ChromeVox and NVDA
2118                                 // These do not seem to inherit aria-disabled from parent elements
2119                                 'aria-disabled': this.isDisabled() ? 'true' : null
2120                         } );
2121                 } else {
2122                         this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
2123                 }
2124         }
2125         return this;
2129  * Handle disable events.
2131  * @private
2132  * @param {boolean} disabled Element is disabled
2133  */
2134 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
2135         this.updateTabIndex();
2139  * Get the value of the tabindex.
2141  * @return {number|null} Tabindex value
2142  */
2143 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
2144         return this.tabIndex;
2148  * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2150  * If the element already has an ID then that is returned, otherwise unique ID is
2151  * generated, set on the element, and returned.
2153  * @return {string|null} The ID of the focusable element
2154  */
2155 OO.ui.mixin.TabIndexedElement.prototype.getInputId = function () {
2156         if ( !this.$tabIndexed ) {
2157                 return null;
2158         }
2159         if ( !this.isLabelableNode( this.$tabIndexed ) ) {
2160                 return null;
2161         }
2163         let id = this.$tabIndexed.attr( 'id' );
2164         if ( id === undefined ) {
2165                 id = OO.ui.generateElementId();
2166                 this.$tabIndexed.attr( 'id', id );
2167         }
2169         return id;
2173  * Whether the node is 'labelable' according to the HTML spec
2174  * (i.e., whether it can be interacted with through a `<label for="…">`).
2175  * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2177  * @private
2178  * @param {jQuery} $node
2179  * @return {boolean}
2180  */
2181 OO.ui.mixin.TabIndexedElement.prototype.isLabelableNode = function ( $node ) {
2182         const
2183                 labelableTags = [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2184                 tagName = ( $node.prop( 'tagName' ) || '' ).toLowerCase();
2186         if ( tagName === 'input' && $node.attr( 'type' ) !== 'hidden' ) {
2187                 return true;
2188         }
2189         if ( labelableTags.indexOf( tagName ) !== -1 ) {
2190                 return true;
2191         }
2192         return false;
2196  * Focus this element.
2198  * @chainable
2199  * @return {OO.ui.Element} The element, for chaining
2200  */
2201 OO.ui.mixin.TabIndexedElement.prototype.focus = function () {
2202         if ( !this.isDisabled() ) {
2203                 this.$tabIndexed.trigger( 'focus' );
2204         }
2205         return this;
2209  * Blur this element.
2211  * @chainable
2212  * @return {OO.ui.Element} The element, for chaining
2213  */
2214 OO.ui.mixin.TabIndexedElement.prototype.blur = function () {
2215         this.$tabIndexed.trigger( 'blur' );
2216         return this;
2220  * @inheritdoc OO.ui.Widget
2221  */
2222 OO.ui.mixin.TabIndexedElement.prototype.simulateLabelClick = function () {
2223         this.focus();
2227  * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2228  * interface element that can be configured with access keys for keyboard interaction.
2229  * See the [OOUI documentation on MediaWiki][1] for examples.
2231  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2233  * @abstract
2234  * @class
2236  * @constructor
2237  * @param {Object} [config] Configuration options
2238  * @param {jQuery} [config.$button] The button element created by the class.
2239  *  If this configuration is omitted, the button element will use a generated `<a>`.
2240  * @param {boolean} [config.framed=true] Render the button with a frame
2241  */
2242 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
2243         // Configuration initialization
2244         config = config || {};
2246         // Properties
2247         this.$button = null;
2248         this.framed = null;
2249         this.active = config.active !== undefined && config.active;
2250         this.onDocumentMouseUpHandler = this.onDocumentMouseUp.bind( this );
2251         this.onMouseDownHandler = this.onMouseDown.bind( this );
2252         this.onDocumentKeyUpHandler = this.onDocumentKeyUp.bind( this );
2253         this.onKeyDownHandler = this.onKeyDown.bind( this );
2254         this.onClickHandler = this.onClick.bind( this );
2255         this.onKeyPressHandler = this.onKeyPress.bind( this );
2257         // Initialization
2258         this.$element.addClass( 'oo-ui-buttonElement' );
2259         this.toggleFramed( config.framed === undefined || config.framed );
2260         this.setButtonElement( config.$button || $( '<a>' ) );
2263 /* Setup */
2265 OO.initClass( OO.ui.mixin.ButtonElement );
2267 /* Static Properties */
2270  * Cancel mouse down events.
2272  * This property is usually set to `true` to prevent the focus from changing when the button is
2273  * clicked.
2274  * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and
2275  * {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} use a value of `false` so that dragging
2276  * behavior is possible and mousedown events can be handled by a parent widget.
2278  * @static
2279  * @property {boolean}
2280  */
2281 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
2283 /* Events */
2286  * A 'click' event is emitted when the button element is clicked.
2288  * @event OO.ui.mixin.ButtonElement#click
2289  */
2291 /* Methods */
2294  * Set the button element.
2296  * This method is used to retarget a button mixin so that its functionality applies to
2297  * the specified button element instead of the one created by the class. If a button element
2298  * is already set, the method will remove the mixin’s effect on that element.
2300  * @param {jQuery} $button Element to use as button
2301  */
2302 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
2303         if ( this.$button ) {
2304                 this.$button
2305                         .removeClass( 'oo-ui-buttonElement-button' )
2306                         .removeAttr( 'role accesskey' )
2307                         .off( {
2308                                 mousedown: this.onMouseDownHandler,
2309                                 keydown: this.onKeyDownHandler,
2310                                 click: this.onClickHandler,
2311                                 keypress: this.onKeyPressHandler
2312                         } );
2313         }
2315         this.$button = $button
2316                 .addClass( 'oo-ui-buttonElement-button' )
2317                 .on( {
2318                         mousedown: this.onMouseDownHandler,
2319                         keydown: this.onKeyDownHandler,
2320                         click: this.onClickHandler,
2321                         keypress: this.onKeyPressHandler
2322                 } );
2324         // Add `role="button"` on `<a>` elements, where it's needed
2325         // `toUpperCase()` is added for XHTML documents
2326         if ( this.$button.prop( 'tagName' ).toUpperCase() === 'A' ) {
2327                 this.$button.attr( 'role', 'button' );
2328         }
2332  * Handles mouse down events.
2334  * @protected
2335  * @param {jQuery.Event} e Mouse down event
2336  * @return {undefined|boolean} False to prevent default if event is handled
2337  */
2338 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
2339         if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
2340                 return;
2341         }
2342         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
2343         // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2344         // reliably remove the pressed class
2345         this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler, true );
2346         // Prevent change of focus unless specifically configured otherwise
2347         if ( this.constructor.static.cancelButtonMouseDownEvents ) {
2348                 return false;
2349         }
2353  * Handles document mouse up events.
2355  * @protected
2356  * @param {MouseEvent} e Mouse up event
2357  */
2358 OO.ui.mixin.ButtonElement.prototype.onDocumentMouseUp = function ( e ) {
2359         if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
2360                 return;
2361         }
2362         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
2363         // Stop listening for mouseup, since we only needed this once
2364         this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler, true );
2368  * Handles mouse click events.
2370  * @protected
2371  * @param {jQuery.Event} e Mouse click event
2372  * @fires OO.ui.mixin.ButtonElement#click
2373  * @return {undefined|boolean} False to prevent default if event is handled
2374  */
2375 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
2376         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
2377                 if ( this.emit( 'click' ) ) {
2378                         return false;
2379                 }
2380         }
2384  * Handles key down events.
2386  * @protected
2387  * @param {jQuery.Event} e Key down event
2388  */
2389 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
2390         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
2391                 return;
2392         }
2393         this.$element.addClass( 'oo-ui-buttonElement-pressed' );
2394         // Run the keyup handler no matter where the key is when the button is let go, so we can
2395         // reliably remove the pressed class
2396         this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler, true );
2400  * Handles document key up events.
2402  * @protected
2403  * @param {KeyboardEvent} e Key up event
2404  */
2405 OO.ui.mixin.ButtonElement.prototype.onDocumentKeyUp = function ( e ) {
2406         if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
2407                 return;
2408         }
2409         this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
2410         // Stop listening for keyup, since we only needed this once
2411         this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler, true );
2415  * Handles key press events.
2417  * @protected
2418  * @param {jQuery.Event} e Key press event
2419  * @fires OO.ui.mixin.ButtonElement#click
2420  * @return {undefined|boolean} False to prevent default if event is handled
2421  */
2422 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
2423         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
2424                 if ( this.emit( 'click' ) ) {
2425                         return false;
2426                 }
2427         }
2431  * Check if button has a frame.
2433  * @return {boolean} Button is framed
2434  */
2435 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
2436         return this.framed;
2440  * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame
2441  * on and off.
2443  * @param {boolean} [framed] Make button framed, omit to toggle
2444  * @chainable
2445  * @return {OO.ui.Element} The element, for chaining
2446  */
2447 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
2448         framed = framed === undefined ? !this.framed : !!framed;
2449         if ( framed !== this.framed ) {
2450                 this.framed = framed;
2451                 this.$element
2452                         .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
2453                         .toggleClass( 'oo-ui-buttonElement-framed', framed );
2454                 this.updateThemeClasses();
2455         }
2457         return this;
2461  * Set the button's active state.
2463  * The active state can be set on:
2465  *  - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2466  *  - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2467  *  - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2469  * @protected
2470  * @param {boolean} [value=false] Make button active
2471  * @chainable
2472  * @return {OO.ui.Element} The element, for chaining
2473  */
2474 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
2475         this.active = !!value;
2476         this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
2477         this.updateThemeClasses();
2478         return this;
2482  * Check if the button is active
2484  * @protected
2485  * @return {boolean} The button is active
2486  */
2487 OO.ui.mixin.ButtonElement.prototype.isActive = function () {
2488         return this.active;
2492  * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2493  * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2494  * items from the group is done through the interface the class provides.
2495  * For more information, please see the [OOUI documentation on MediaWiki][1].
2497  * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2499  * @abstract
2500  * @mixes OO.EmitterList
2501  * @class
2503  * @constructor
2504  * @param {Object} [config] Configuration options
2505  * @param {jQuery} [config.$group] The container element created by the class. If this configuration
2506  *  is omitted, the group element will use a generated `<div>`.
2507  */
2508 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
2509         // Configuration initialization
2510         config = config || {};
2512         // Mixin constructors
2513         OO.EmitterList.call( this, config );
2515         // Properties
2516         this.$group = null;
2518         // Initialization
2519         this.setGroupElement( config.$group || $( '<div>' ) );
2522 /* Setup */
2524 OO.mixinClass( OO.ui.mixin.GroupElement, OO.EmitterList );
2526 /* Events */
2529  * A change event is emitted when the set of selected items changes.
2531  * @event OO.ui.mixin.GroupElement#change
2532  * @param {OO.ui.Element[]} items Items currently in the group
2533  */
2535 /* Methods */
2538  * Set the group element.
2540  * If an element is already set, items will be moved to the new element.
2542  * @param {jQuery} $group Element to use as group
2543  */
2544 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
2545         this.$group = $group;
2546         for ( let i = 0, len = this.items.length; i < len; i++ ) {
2547                 this.$group.append( this.items[ i ].$element );
2548         }
2552  * Find an item by its data.
2554  * Only the first item with matching data will be returned. To return all matching items,
2555  * use the #findItemsFromData method.
2557  * @param {any} data Item data to search for
2558  * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2559  */
2560 OO.ui.mixin.GroupElement.prototype.findItemFromData = function ( data ) {
2561         const hash = OO.getHash( data );
2563         for ( let i = 0, len = this.items.length; i < len; i++ ) {
2564                 const item = this.items[ i ];
2565                 if ( hash === OO.getHash( item.getData() ) ) {
2566                         return item;
2567                 }
2568         }
2570         return null;
2574  * Find items by their data.
2576  * All items with matching data will be returned. To return only the first match, use the
2577  * #findItemFromData method instead.
2579  * @param {any} data Item data to search for
2580  * @return {OO.ui.Element[]} Items with equivalent data
2581  */
2582 OO.ui.mixin.GroupElement.prototype.findItemsFromData = function ( data ) {
2583         const hash = OO.getHash( data ),
2584                 items = [];
2586         for ( let i = 0, len = this.items.length; i < len; i++ ) {
2587                 const item = this.items[ i ];
2588                 if ( hash === OO.getHash( item.getData() ) ) {
2589                         items.push( item );
2590                 }
2591         }
2593         return items;
2597  * Add items to the group.
2599  * Items will be added to the end of the group array unless the optional `index` parameter
2600  * specifies a different insertion point. Adding an existing item will move it to the end of the
2601  * array or the point specified by the `index`.
2603  * @param {OO.ui.Element|OO.ui.Element[]} [items] Elements to add to the group
2604  * @param {number} [index] Index of the insertion point
2605  * @chainable
2606  * @return {OO.ui.Element} The element, for chaining
2607  */
2608 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
2609         if ( !items || items.length === 0 ) {
2610                 return this;
2611         }
2613         // Mixin method
2614         OO.EmitterList.prototype.addItems.call( this, items, index );
2616         this.emit( 'change', this.getItems() );
2617         return this;
2621  * Move an item from its current position to a new index.
2623  * The item is expected to exist in the list. If it doesn't,
2624  * the method will throw an exception.
2626  * See https://doc.wikimedia.org/oojs/master/OO.EmitterList.html
2628  * @private
2629  * @param {OO.EventEmitter} items Item to add
2630  * @param {number} newIndex Index to move the item to
2631  * @return {number} The index the item was moved to
2632  * @throws {Error} If item is not in the list
2633  */
2634 OO.ui.mixin.GroupElement.prototype.moveItem = function ( items, newIndex ) {
2635         // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2636         this.insertItemElements( items, newIndex );
2638         // Mixin method
2639         newIndex = OO.EmitterList.prototype.moveItem.call( this, items, newIndex );
2641         return newIndex;
2645  * Utility method to insert an item into the list, and
2646  * connect it to aggregate events.
2648  * Don't call this directly unless you know what you're doing.
2649  * Use #addItems instead.
2651  * This method can be extended in child classes to produce
2652  * different behavior when an item is inserted. For example,
2653  * inserted items may also be attached to the DOM or may
2654  * interact with some other nodes in certain ways. Extending
2655  * this method is allowed, but if overridden, the aggregation
2656  * of events must be preserved, or behavior of emitted events
2657  * will be broken.
2659  * If you are extending this method, please make sure the
2660  * parent method is called.
2662  * See https://doc.wikimedia.org/oojs/master/OO.EmitterList.html
2664  * @protected
2665  * @param {OO.EventEmitter|Object} item Item to add
2666  * @param {number} index Index to add items at
2667  * @return {number} The index the item was added at
2668  */
2669 OO.ui.mixin.GroupElement.prototype.insertItem = function ( item, index ) {
2670         item.setElementGroup( this );
2671         this.insertItemElements( item, index );
2673         // Mixin method
2674         index = OO.EmitterList.prototype.insertItem.call( this, item, index );
2676         return index;
2680  * Insert elements into the group
2682  * @private
2683  * @param {OO.ui.Element} item Item to insert
2684  * @param {number} index Insertion index
2685  */
2686 OO.ui.mixin.GroupElement.prototype.insertItemElements = function ( item, index ) {
2687         if ( index === undefined || index < 0 || index >= this.items.length ) {
2688                 this.$group.append( item.$element );
2689         } else if ( index === 0 ) {
2690                 this.$group.prepend( item.$element );
2691         } else {
2692                 this.items[ index ].$element.before( item.$element );
2693         }
2697  * Remove the specified items from a group.
2699  * Removed items are detached (not removed) from the DOM so that they may be reused.
2700  * To remove all items from a group, you may wish to use the #clearItems method instead.
2702  * @param {OO.ui.Element[]} items An array of items to remove
2703  * @chainable
2704  * @return {OO.ui.Element} The element, for chaining
2705  */
2706 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
2707         if ( items.length === 0 ) {
2708                 return this;
2709         }
2711         // Remove specific items elements
2712         for ( let i = 0, len = items.length; i < len; i++ ) {
2713                 const item = items[ i ];
2714                 const index = this.items.indexOf( item );
2715                 if ( index !== -1 ) {
2716                         item.setElementGroup( null );
2717                         item.$element.detach();
2718                 }
2719         }
2721         // Mixin method
2722         OO.EmitterList.prototype.removeItems.call( this, items );
2724         this.emit( 'change', this.getItems() );
2725         return this;
2729  * Clear all items from the group.
2731  * Cleared items are detached from the DOM, not removed, so that they may be reused.
2732  * To remove only a subset of items from a group, use the #removeItems method.
2734  * @chainable
2735  * @return {OO.ui.Element} The element, for chaining
2736  */
2737 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
2738         // Remove all item elements
2739         for ( let i = 0, len = this.items.length; i < len; i++ ) {
2740                 this.items[ i ].setElementGroup( null );
2741                 this.items[ i ].$element.detach();
2742         }
2744         // Mixin method
2745         OO.EmitterList.prototype.clearItems.call( this );
2747         this.emit( 'change', this.getItems() );
2748         return this;
2752  * LabelElement is often mixed into other classes to generate a label, which
2753  * helps identify the function of an interface element.
2754  * See the [OOUI documentation on MediaWiki][1] for more information.
2756  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2758  * @abstract
2759  * @class
2761  * @constructor
2762  * @param {Object} [config] Configuration options
2763  * @param {jQuery} [config.$label] The label element created by the class. If this
2764  *  configuration is omitted, the label element will use a generated `<span>`.
2765  * @param {jQuery|string|Function|OO.ui.HtmlSnippet} [config.label] The label text. The label can be
2766  *  specified as a plaintext string, a jQuery selection of elements, or a function that will
2767  *  produce a string in the future. See the [OOUI documentation on MediaWiki][2] for examples.
2768  *  [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2769  * @param {boolean} [config.invisibleLabel=false] Whether the label should be visually hidden (but still
2770  *  accessible to screen-readers).
2771  */
2772 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
2773         // Configuration initialization
2774         config = config || {};
2776         // Properties
2777         this.$label = null;
2778         this.label = null;
2779         this.invisibleLabel = false;
2781         // Initialization
2782         this.setLabel( config.label || this.constructor.static.label );
2783         this.setLabelElement( config.$label || $( '<span>' ) );
2784         this.setInvisibleLabel( config.invisibleLabel );
2787 /* Setup */
2789 OO.initClass( OO.ui.mixin.LabelElement );
2791 /* Events */
2794  * @event OO.ui.mixin.LabelElement#labelChange
2795  */
2797 /* Static Properties */
2800  * The label text. The label can be specified as a plaintext string, a function that will
2801  * produce a string (will be resolved on construction time), or `null` for no label. The static
2802  * value will be overridden if a label is specified with the #label config option.
2804  * @static
2805  * @property {string|Function|null}
2806  */
2807 OO.ui.mixin.LabelElement.static.label = null;
2809 /* Static methods */
2812  * Highlight the first occurrence of the query in the given text
2814  * @param {string} text Text
2815  * @param {string} query Query to find
2816  * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2817  * @param {boolean} [combineMarks=false] Pull combining marks into highlighted text
2818  * @return {jQuery} Text with the first match of the query
2819  *  sub-string wrapped in highlighted span
2820  */
2821 OO.ui.mixin.LabelElement.static.highlightQuery = function ( text, query, compare, combineMarks ) {
2822         let offset = -1,
2823                 comboLength = 0,
2824                 comboMarks = '',
2825                 comboRegex,
2826                 comboMatch;
2828         const $result = $( '<span>' );
2830         if ( compare ) {
2831                 const tLen = text.length;
2832                 const qLen = query.length;
2833                 for ( let i = 0; offset === -1 && i <= tLen - qLen; i++ ) {
2834                         if ( compare( query, text.slice( i, i + qLen ) ) === 0 ) {
2835                                 offset = i;
2836                         }
2837                 }
2838         } else {
2839                 offset = text.toLowerCase().indexOf( query.toLowerCase() );
2840         }
2842         if ( !query.length || offset === -1 ) {
2843                 $result.text( text );
2844         } else {
2845                 // Look for combining characters after the match
2846                 if ( combineMarks ) {
2847                         // Equivalent to \p{Mark} (which is not currently available in JavaScript)
2848                         comboMarks = '[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]';
2850                         comboRegex = new RegExp( '(^)' + comboMarks + '*' );
2851                         comboMatch = text.slice( offset + query.length ).match( comboRegex );
2853                         if ( comboMatch && comboMatch.length ) {
2854                                 comboLength = comboMatch[ 0 ].length;
2855                         }
2856                 }
2858                 $result.append(
2859                         document.createTextNode( text.slice( 0, offset ) ),
2860                         $( '<span>' )
2861                                 .addClass( 'oo-ui-labelElement-label-highlight' )
2862                                 .text( text.slice( offset, offset + query.length + comboLength ) ),
2863                         document.createTextNode( text.slice( offset + query.length + comboLength ) )
2864                 );
2865         }
2866         return $result.contents();
2869 /* Methods */
2872  * Replace the wrapper element (an empty `<span>` by default) with another one (e.g. an
2873  * `<a href="…">`), without touching the label's content. This is the same as using the "$label"
2874  * config on construction time.
2876  * If an element is already set, it will be cleaned up before setting up the new element.
2878  * @param {jQuery} $label Element to use as label
2879  * @chainable
2880  * @return {OO.ui.mixin.LabelElement} The element, for chaining
2881  */
2882 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
2883         if ( this.$label ) {
2884                 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
2885         }
2887         this.$label = $label.addClass( 'oo-ui-labelElement-label' );
2888         this.setLabelContent( this.label );
2889         return this;
2893  * Set the 'id' attribute of the label element.
2895  * @param {string} id
2896  * @chainable
2897  * @return {OO.ui.mixin.LabelElement} The element, for chaining
2898  */
2899 OO.ui.mixin.LabelElement.prototype.setLabelId = function ( id ) {
2900         this.$label.attr( 'id', id );
2901         return this;
2905  * Replace both the visible content of the label (same as #setLabelContent) as well as the value
2906  * returned by #getLabel, without touching the label's wrapper element. This is the same as using
2907  * the "label" config on construction time.
2909  * An empty string will result in the label being hidden. A string containing only whitespace will
2910  * be converted to a single `&nbsp;`.
2912  * To change the wrapper element, use #setLabelElement or the "$label" config.
2914  * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that
2915  *  returns nodes or text; or null for no label
2916  * @chainable
2917  * @return {OO.ui.Element} The element, for chaining
2918  */
2919 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
2920         label = OO.ui.resolveMsg( label );
2921         label = ( ( typeof label === 'string' || label instanceof $ ) && label.length ) || ( label instanceof OO.ui.HtmlSnippet && label.toString().length ) ? label : null;
2923         if ( this.label !== label ) {
2924                 if ( this.$label ) {
2925                         this.setLabelContent( label );
2926                 }
2927                 this.label = label;
2928                 this.$element.toggleClass( 'oo-ui-labelElement', !!this.label && !this.invisibleLabel );
2929                 this.emit( 'labelChange' );
2930         }
2932         return this;
2936  * Set whether the label should be visually hidden (but still accessible to screen-readers).
2938  * @param {boolean} [invisibleLabel=false]
2939  * @chainable
2940  * @return {OO.ui.Element} The element, for chaining
2941  */
2942 OO.ui.mixin.LabelElement.prototype.setInvisibleLabel = function ( invisibleLabel ) {
2943         invisibleLabel = !!invisibleLabel;
2945         if ( this.invisibleLabel !== invisibleLabel ) {
2946                 this.invisibleLabel = invisibleLabel;
2947                 this.$label.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel );
2948                 // Pretend that there is no label, a lot of CSS has been written with this assumption
2949                 this.$element.toggleClass( 'oo-ui-labelElement', !!this.label && !this.invisibleLabel );
2950                 this.emit( 'labelChange' );
2951         }
2953         return this;
2957  * Set the label as plain text with a highlighted query
2959  * @param {string} text Text label to set
2960  * @param {string} query Substring of text to highlight
2961  * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2962  * @param {boolean} [combineMarks=false] Pull combining marks into highlighted text
2963  * @chainable
2964  * @return {OO.ui.Element} The element, for chaining
2965  */
2966 OO.ui.mixin.LabelElement.prototype.setHighlightedQuery = function (
2967         text, query, compare, combineMarks
2968 ) {
2969         return this.setLabel(
2970                 this.constructor.static.highlightQuery( text, query, compare, combineMarks )
2971         );
2975  * Get the label's value as provided via #setLabel or the "label" config. Note this is not
2976  * necessarily the same as the label's visible content when #setLabelContent was used.
2978  * @return {jQuery|string|null} Label nodes; text; or null for no label
2979  */
2980 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
2981         return this.label;
2985  * Replace the visible content of the label, without touching it's wrapper element. Note this is not
2986  * the same as using the "label" config on construction time. #setLabelContent does not change the
2987  * value returned by #getLabel.
2989  * To change the value as well, use #setLabel or the "label" config. To change the wrapper element,
2990  * use #setLabelElement or the "$label" config.
2992  * @private
2993  * @param {jQuery|string|null} label Label nodes; text; or null for no label
2994  */
2995 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
2996         if ( typeof label === 'string' ) {
2997                 if ( label.match( /^\s*$/ ) ) {
2998                         // Convert whitespace only string to a single non-breaking space
2999                         this.$label.html( '&nbsp;' );
3000                 } else {
3001                         this.$label.text( label );
3002                 }
3003         } else if ( label instanceof OO.ui.HtmlSnippet ) {
3004                 this.$label.html( label.toString() );
3005         } else if ( label instanceof $ ) {
3006                 this.$label.empty().append( label );
3007         } else {
3008                 this.$label.empty();
3009         }
3013  * IconElement is often mixed into other classes to generate an icon.
3014  * Icons are graphics, about the size of normal text. They are used to aid the user
3015  * in locating a control or to convey information in a space-efficient way. See the
3016  * [OOUI documentation on MediaWiki][1] for a list of icons
3017  * included in the library.
3019  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
3021  * @abstract
3022  * @class
3024  * @constructor
3025  * @param {Object} [config] Configuration options
3026  * @param {jQuery} [config.$icon] The icon element created by the class. If this configuration is omitted,
3027  *  the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
3028  *  the icon element be set to an existing icon instead of the one generated by this class, set a
3029  *  value using a jQuery selection. For example:
3031  *      // Use a <div> tag instead of a <span>
3032  *     $icon: $( '<div>' )
3033  *     // Use an existing icon element instead of the one generated by the class
3034  *     $icon: this.$element
3035  *     // Use an icon element from a child widget
3036  *     $icon: this.childwidget.$element
3037  * @param {Object|string} [config.icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a
3038  *  map of symbolic names. A map is used for i18n purposes and contains a `default` icon
3039  *  name and additional names keyed by language code. The `default` name is used when no icon is
3040  *  keyed by the user's language.
3042  *  Example of an i18n map:
3044  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
3045  *  See the [OOUI documentation on MediaWiki][2] for a list of icons included in the library.
3046  * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
3047  */
3048 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
3049         // Configuration initialization
3050         config = config || {};
3052         // Properties
3053         this.$icon = null;
3054         this.icon = null;
3056         // Initialization
3057         this.setIcon( config.icon || this.constructor.static.icon );
3058         this.setIconElement( config.$icon || $( '<span>' ) );
3061 /* Setup */
3063 OO.initClass( OO.ui.mixin.IconElement );
3065 /* Static Properties */
3068  * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map
3069  * is used for i18n purposes and contains a `default` icon name and additional names keyed by
3070  * language code. The `default` name is used when no icon is keyed by the user's language.
3072  * Example of an i18n map:
3074  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
3076  * Note: the static property will be overridden if the #icon configuration is used.
3078  * @static
3079  * @property {Object|string}
3080  */
3081 OO.ui.mixin.IconElement.static.icon = null;
3084  * The icon title, displayed when users move the mouse over the icon. The value can be text, a
3085  * function that returns title text, or `null` for no title.
3087  * The static property will be overridden if the #iconTitle configuration is used.
3089  * @static
3090  * @property {string|Function|null}
3091  */
3092 OO.ui.mixin.IconElement.static.iconTitle = null;
3094 /* Methods */
3097  * Set the icon element. This method is used to retarget an icon mixin so that its functionality
3098  * applies to the specified icon element instead of the one created by the class. If an icon
3099  * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
3100  * and mixin methods will no longer affect the element.
3102  * @param {jQuery} $icon Element to use as icon
3103  */
3104 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
3105         if ( this.$icon ) {
3106                 this.$icon
3107                         .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
3108                         .removeAttr( 'title' );
3109         }
3111         this.$icon = $icon
3112                 .addClass( 'oo-ui-iconElement-icon' )
3113                 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon )
3114                 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
3115         if ( this.iconTitle !== null ) {
3116                 this.$icon.attr( 'title', this.iconTitle );
3117         }
3119         this.updateThemeClasses();
3123  * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
3124  * The icon parameter can also be set to a map of icon names. See the #icon config setting
3125  * for an example.
3127  * @param {Object|string|null} icon A symbolic icon name, a {@link OO.ui.mixin.IconElement.static.icon map of icon names} keyed
3128  *  by language code, or `null` to remove the icon.
3129  * @chainable
3130  * @return {OO.ui.Element} The element, for chaining
3131  */
3132 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
3133         if ( icon && typeof icon !== 'string' ) {
3134                 icon = OO.ui.getLocalValue( icon, null, 'default' );
3135         }
3137         if ( this.icon === icon ) {
3138                 return this;
3139         }
3141         this.$element.toggleClass( 'oo-ui-iconElement', !!icon );
3142         if ( this.$icon ) {
3143                 if ( this.icon ) {
3144                         this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
3145                 }
3146                 if ( icon ) {
3147                         this.$icon.addClass( 'oo-ui-icon-' + icon );
3148                 }
3149                 this.$icon.toggleClass( 'oo-ui-iconElement-noIcon', !icon );
3150         }
3152         this.icon = icon;
3153         this.updateThemeClasses();
3155         return this;
3159  * Get the symbolic name of the icon.
3161  * @return {string} Icon name
3162  */
3163 OO.ui.mixin.IconElement.prototype.getIcon = function () {
3164         return this.icon;
3168  * IndicatorElement is often mixed into other classes to generate an indicator.
3169  * Indicators are small graphics that are generally used in two ways:
3171  * - To draw attention to the status of an item. For example, an indicator might be
3172  *   used to show that an item in a list has errors that need to be resolved.
3173  * - To clarify the function of a control that acts in an exceptional way (a button
3174  *   that opens a menu instead of performing an action directly, for example).
3176  * For a list of indicators included in the library, please see the
3177  * [OOUI documentation on MediaWiki][1].
3179  * Note that indicators don't come with any functionality by default. See e.g.
3180  * {@link OO.ui.SearchInputWidget SearchInputWidget} for a working 'clear' or
3181  * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget} for a working 'down' indicator.
3183  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3185  * @abstract
3186  * @class
3188  * @constructor
3189  * @param {Object} [config] Configuration options
3190  * @param {jQuery} [config.$indicator] The indicator element created by the class. If this
3191  *  configuration is omitted, the indicator element will use a generated `<span>`.
3192  * @param {string} [config.indicator] Symbolic name of the indicator (e.g. ‘required’ or ‘down’).
3193  *  See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3194  *  in the library.
3195  * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3196  */
3197 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
3198         // Configuration initialization
3199         config = config || {};
3201         // Properties
3202         this.$indicator = null;
3203         this.indicator = null;
3205         // Initialization
3206         this.setIndicator( config.indicator || this.constructor.static.indicator );
3207         this.setIndicatorElement( config.$indicator || $( '<span>' ) );
3210 /* Setup */
3212 OO.initClass( OO.ui.mixin.IndicatorElement );
3214 /* Static Properties */
3217  * Symbolic name of the indicator (e.g. ‘required’ or ‘down’).
3218  * The static property will be overridden if the #indicator configuration is used.
3220  * @static
3221  * @property {string|null}
3222  */
3223 OO.ui.mixin.IndicatorElement.static.indicator = null;
3226  * A text string used as the indicator title, a function that returns title text, or `null`
3227  * for no title. The static property will be overridden if the #indicatorTitle configuration is
3228  * used.
3230  * @static
3231  * @property {string|Function|null}
3232  */
3233 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
3235 /* Methods */
3238  * Set the indicator element.
3240  * If an element is already set, it will be cleaned up before setting up the new element.
3242  * @param {jQuery} $indicator Element to use as indicator
3243  */
3244 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
3245         if ( this.$indicator ) {
3246                 this.$indicator
3247                         .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
3248                         .removeAttr( 'title' );
3249         }
3251         this.$indicator = $indicator
3252                 .addClass( 'oo-ui-indicatorElement-indicator' )
3253                 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator )
3254                 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
3255         if ( this.indicatorTitle !== null ) {
3256                 this.$indicator.attr( 'title', this.indicatorTitle );
3257         }
3259         this.updateThemeClasses();
3263  * Set the indicator by its symbolic name. Built-in names currently include ‘clear’, ‘up’,
3264  * ‘down’ and ‘required’ (declared via indicators.json). Use `null` to remove the indicator.
3266  * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3267  * @chainable
3268  * @return {OO.ui.Element} The element, for chaining
3269  */
3270 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
3271         indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
3273         if ( this.indicator !== indicator ) {
3274                 if ( this.$indicator ) {
3275                         if ( this.indicator !== null ) {
3276                                 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
3277                         }
3278                         if ( indicator !== null ) {
3279                                 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
3280                         }
3281                 }
3282                 this.indicator = indicator;
3283         }
3285         this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
3286         if ( this.$indicator ) {
3287                 this.$indicator.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator );
3288         }
3289         this.updateThemeClasses();
3291         return this;
3295  * Get the symbolic name of the indicator (e.g., ‘required’ or ‘down’).
3297  * @return {string|null} Symbolic name of indicator, null if not set
3298  */
3299 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
3300         return this.indicator;
3304  * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3305  * additional functionality to an element created by another class. The class provides
3306  * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3307  * which are used to customize the look and feel of a widget to better describe its
3308  * importance and functionality.
3310  * The library currently contains the following styling flags for general use:
3312  * - **progressive**: Progressive styling is applied to convey that the widget will move the user
3313  *   forward in a process.
3314  * - **destructive**: Destructive styling is applied to convey that the widget will remove
3315  *   something.
3317  * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an
3318  * action, use these flags: **primary** and **safe**.
3319  * Please see the [OOUI documentation on MediaWiki][1] for more information.
3321  * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3323  * The flags affect the appearance of the buttons:
3325  *     @example
3326  *     // FlaggedElement is mixed into ButtonWidget to provide styling flags
3327  *     const button1 = new OO.ui.ButtonWidget( {
3328  *             label: 'Progressive',
3329  *             flags: 'progressive'
3330  *         } ),
3331  *         button2 = new OO.ui.ButtonWidget( {
3332  *             label: 'Destructive',
3333  *             flags: 'destructive'
3334  *         } );
3335  *     $( document.body ).append( button1.$element, button2.$element );
3337  * @abstract
3338  * @class
3340  * @constructor
3341  * @param {Object} [config] Configuration options
3342  * @param {string|string[]} [config.flags] The name or names of the flags (e.g., 'progressive' or 'primary')
3343  *  to apply.
3344  *  Please see the [OOUI documentation on MediaWiki][2] for more information about available flags.
3345  *  [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3346  * @param {jQuery} [config.$flagged] The flagged element. By default,
3347  *  the flagged functionality is applied to the element created by the class ($element).
3348  *  If a different element is specified, the flagged functionality will be applied to it instead.
3349  */
3350 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
3351         // Configuration initialization
3352         config = config || {};
3354         // Properties
3355         this.flags = {};
3356         this.$flagged = null;
3358         // Initialization
3359         this.setFlags( config.flags || this.constructor.static.flags );
3360         this.setFlaggedElement( config.$flagged || this.$element );
3363 /* Setup */
3365 OO.initClass( OO.ui.mixin.FlaggedElement );
3367 /* Events */
3370  * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3371  * parameter contains the name of each modified flag and indicates whether it was
3372  * added or removed.
3374  * @event OO.ui.mixin.FlaggedElement#flag
3375  * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3376  * that the flag was added, `false` that the flag was removed.
3377  */
3379 /* Static Properties */
3382  * Initial value to pass to setFlags if no value is provided in config.
3384  * @static
3385  * @property {string|string[]|Object.<string, boolean>}
3386  */
3387 OO.ui.mixin.FlaggedElement.static.flags = null;
3389 /* Methods */
3392  * Set the flagged element.
3394  * This method is used to retarget a flagged mixin so that its functionality applies to the
3395  * specified element.
3396  * If an element is already set, the method will remove the mixin’s effect on that element.
3398  * @param {jQuery} $flagged Element that should be flagged
3399  */
3400 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
3401         const classNames = Object.keys( this.flags ).map( ( flag ) => 'oo-ui-flaggedElement-' + flag );
3403         if ( this.$flagged ) {
3404                 this.$flagged.removeClass( classNames );
3405         }
3407         this.$flagged = $flagged.addClass( classNames );
3411  * Check if the specified flag is set.
3413  * @param {string} flag Name of flag
3414  * @return {boolean} The flag is set
3415  */
3416 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
3417         // This may be called before the constructor, thus before this.flags is set
3418         return this.flags && ( flag in this.flags );
3422  * Get the names of all flags set.
3424  * @return {string[]} Flag names
3425  */
3426 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
3427         // This may be called before the constructor, thus before this.flags is set
3428         return Object.keys( this.flags || {} );
3432  * Clear all flags.
3434  * @chainable
3435  * @return {OO.ui.Element} The element, for chaining
3436  * @fires OO.ui.mixin.FlaggedElement#flag
3437  */
3438 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
3439         const changes = {},
3440                 remove = [],
3441                 classPrefix = 'oo-ui-flaggedElement-';
3443         for ( const flag in this.flags ) {
3444                 const className = classPrefix + flag;
3445                 changes[ flag ] = false;
3446                 delete this.flags[ flag ];
3447                 remove.push( className );
3448         }
3450         if ( this.$flagged ) {
3451                 this.$flagged.removeClass( remove );
3452         }
3454         this.updateThemeClasses();
3455         this.emit( 'flag', changes );
3457         return this;
3461  * Add one or more flags.
3463  * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3464  *  or an object keyed by flag name with a boolean value that indicates whether the flag should
3465  *  be added (`true`) or removed (`false`).
3466  * @chainable
3467  * @return {OO.ui.Element} The element, for chaining
3468  * @fires OO.ui.mixin.FlaggedElement#flag
3469  */
3470 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
3471         const changes = {},
3472                 add = [],
3473                 remove = [],
3474                 classPrefix = 'oo-ui-flaggedElement-';
3476         let className, flag;
3477         if ( typeof flags === 'string' ) {
3478                 className = classPrefix + flags;
3479                 // Set
3480                 if ( !this.flags[ flags ] ) {
3481                         this.flags[ flags ] = true;
3482                         add.push( className );
3483                 }
3484         } else if ( Array.isArray( flags ) ) {
3485                 for ( let i = 0, len = flags.length; i < len; i++ ) {
3486                         flag = flags[ i ];
3487                         className = classPrefix + flag;
3488                         // Set
3489                         if ( !this.flags[ flag ] ) {
3490                                 changes[ flag ] = true;
3491                                 this.flags[ flag ] = true;
3492                                 add.push( className );
3493                         }
3494                 }
3495         } else if ( OO.isPlainObject( flags ) ) {
3496                 for ( flag in flags ) {
3497                         className = classPrefix + flag;
3498                         if ( flags[ flag ] ) {
3499                                 // Set
3500                                 if ( !this.flags[ flag ] ) {
3501                                         changes[ flag ] = true;
3502                                         this.flags[ flag ] = true;
3503                                         add.push( className );
3504                                 }
3505                         } else {
3506                                 // Remove
3507                                 if ( this.flags[ flag ] ) {
3508                                         changes[ flag ] = false;
3509                                         delete this.flags[ flag ];
3510                                         remove.push( className );
3511                                 }
3512                         }
3513                 }
3514         }
3516         if ( this.$flagged ) {
3517                 this.$flagged
3518                         .addClass( add )
3519                         .removeClass( remove );
3520         }
3522         this.updateThemeClasses();
3523         this.emit( 'flag', changes );
3525         return this;
3529  * TitledElement is mixed into other classes to provide a `title` attribute.
3530  * Titles are rendered by the browser and are made visible when the user moves
3531  * the mouse over the element. Titles are not visible on touch devices.
3533  *     @example
3534  *     // TitledElement provides a `title` attribute to the
3535  *     // ButtonWidget class.
3536  *     const button = new OO.ui.ButtonWidget( {
3537  *         label: 'Button with Title',
3538  *         title: 'I am a button'
3539  *     } );
3540  *     $( document.body ).append( button.$element );
3542  * @abstract
3543  * @class
3545  * @constructor
3546  * @param {Object} [config] Configuration options
3547  * @param {jQuery} [config.$titled] The element to which the `title` attribute is applied.
3548  *  If this config is omitted, the title functionality is applied to $element, the
3549  *  element created by the class.
3550  * @param {string|Function} [config.title] The title text or a function that returns text. If
3551  *  this config is omitted, the value of the {@link OO.ui.mixin.TitledElement.static.title static title} property is used.
3552  *  If config for an invisible label ({@link OO.ui.mixin.LabelElement}) is present, and a title is
3553  *  omitted, the label will be used as a fallback for the title.
3554  */
3555 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
3556         // Configuration initialization
3557         config = config || {};
3559         // Properties
3560         this.$titled = null;
3561         this.title = null;
3563         // Initialization
3564         let title = config.title !== undefined ? config.title : this.constructor.static.title;
3565         if (
3566                 title === null &&
3567                 config.invisibleLabel &&
3568                 typeof config.label === 'string'
3569         ) {
3570                 // If config for an invisible label is present, use this as a fallback title
3571                 title = config.label;
3572         }
3573         this.setTitle( title );
3574         this.setTitledElement( config.$titled || this.$element );
3577 /* Setup */
3579 OO.initClass( OO.ui.mixin.TitledElement );
3581 /* Static Properties */
3584  * The title text, a function that returns text, or `null` for no title. The value of the static
3585  * property is overridden if the #title config option is used.
3587  * If the element has a default title (e.g. `<input type=file>`), `null` will allow that title to be
3588  * shown. Use empty string to suppress it.
3590  * @static
3591  * @property {string|Function|null}
3592  */
3593 OO.ui.mixin.TitledElement.static.title = null;
3595 /* Methods */
3598  * Set the titled element.
3600  * This method is used to retarget a TitledElement mixin so that its functionality applies to the
3601  * specified element.
3602  * If an element is already set, the mixin’s effect on that element is removed before the new
3603  * element is set up.
3605  * @param {jQuery} $titled Element that should use the 'titled' functionality
3606  */
3607 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
3608         if ( this.$titled ) {
3609                 this.$titled.removeAttr( 'title' );
3610         }
3612         this.$titled = $titled;
3613         this.updateTitle();
3617  * Set title.
3619  * @param {string|Function|null} title Title text, a function that returns text, or `null`
3620  *  for no title
3621  * @chainable
3622  * @return {OO.ui.Element} The element, for chaining
3623  */
3624 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
3625         title = OO.ui.resolveMsg( title );
3626         title = typeof title === 'string' ? title : null;
3628         if ( this.title !== title ) {
3629                 this.title = title;
3630                 this.updateTitle();
3631         }
3633         return this;
3637  * Update the title attribute, in case of changes to title or accessKey.
3639  * @protected
3640  * @chainable
3641  * @return {OO.ui.Element} The element, for chaining
3642  */
3643 OO.ui.mixin.TitledElement.prototype.updateTitle = function () {
3644         let title = this.getTitle();
3645         if ( this.$titled ) {
3646                 if ( title !== null ) {
3647                         // Only if this is an AccessKeyedElement
3648                         if ( this.formatTitleWithAccessKey ) {
3649                                 title = this.formatTitleWithAccessKey( title );
3650                         }
3651                         this.$titled.attr( 'title', title );
3652                 } else {
3653                         this.$titled.removeAttr( 'title' );
3654                 }
3655         }
3656         return this;
3660  * Get title.
3662  * @return {string|null} Title string
3663  */
3664 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
3665         return this.title;
3669  * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
3670  * Access keys allow an user to go to a specific element by using
3671  * a shortcut combination of a browser specific keys + the key
3672  * set to the field.
3674  *     @example
3675  *     // AccessKeyedElement provides an `accesskey` attribute to the
3676  *     // ButtonWidget class.
3677  *     const button = new OO.ui.ButtonWidget( {
3678  *         label: 'Button with access key',
3679  *         accessKey: 'k'
3680  *     } );
3681  *     $( document.body ).append( button.$element );
3683  * @abstract
3684  * @class
3686  * @constructor
3687  * @param {Object} [config] Configuration options
3688  * @param {jQuery} [config.$accessKeyed] The element to which the `accesskey` attribute is applied.
3689  *  If this config is omitted, the access key functionality is applied to $element, the
3690  *  element created by the class.
3691  * @param {string|Function|null} [config.accessKey=null] The key or a function that returns the key. If
3692  *  this config is omitted, no access key will be added.
3693  */
3694 OO.ui.mixin.AccessKeyedElement = function OoUiMixinAccessKeyedElement( config ) {
3695         // Configuration initialization
3696         config = config || {};
3698         // Properties
3699         this.$accessKeyed = null;
3700         this.accessKey = null;
3702         // Initialization
3703         this.setAccessKey( config.accessKey || null );
3704         this.setAccessKeyedElement( config.$accessKeyed || this.$element );
3706         // If this is also a TitledElement and it initialized before we did, we may have
3707         // to update the title with the access key
3708         if ( this.updateTitle ) {
3709                 this.updateTitle();
3710         }
3713 /* Setup */
3715 OO.initClass( OO.ui.mixin.AccessKeyedElement );
3717 /* Static Properties */
3720  * The access key, a function that returns a key, or `null` for no access key.
3722  * @static
3723  * @property {string|Function|null}
3724  */
3725 OO.ui.mixin.AccessKeyedElement.static.accessKey = null;
3727 /* Methods */
3730  * Set the access keyed element.
3732  * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to
3733  * the specified element.
3734  * If an element is already set, the mixin's effect on that element is removed before the new
3735  * element is set up.
3737  * @param {jQuery} $accessKeyed Element that should use the 'access keyed' functionality
3738  */
3739 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKeyedElement = function ( $accessKeyed ) {
3740         if ( this.$accessKeyed ) {
3741                 this.$accessKeyed.removeAttr( 'accesskey' );
3742         }
3744         this.$accessKeyed = $accessKeyed;
3745         if ( this.accessKey ) {
3746                 this.$accessKeyed.attr( 'accesskey', this.accessKey );
3747         }
3751  * Set access key.
3753  * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no
3754  *  access key
3755  * @chainable
3756  * @return {OO.ui.Element} The element, for chaining
3757  */
3758 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKey = function ( accessKey ) {
3759         accessKey = OO.ui.resolveMsg( accessKey );
3760         accessKey = typeof accessKey === 'string' ? accessKey : null;
3762         if ( this.accessKey !== accessKey ) {
3763                 if ( this.$accessKeyed ) {
3764                         if ( accessKey !== null ) {
3765                                 this.$accessKeyed.attr( 'accesskey', accessKey );
3766                         } else {
3767                                 this.$accessKeyed.removeAttr( 'accesskey' );
3768                         }
3769                 }
3770                 this.accessKey = accessKey;
3772                 // Only if this is a TitledElement
3773                 if ( this.updateTitle ) {
3774                         this.updateTitle();
3775                 }
3776         }
3778         return this;
3782  * Get access key.
3784  * @return {string} accessKey string
3785  */
3786 OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
3787         return this.accessKey;
3791  * Add information about the access key to the element's tooltip label.
3792  * (This is only public for hacky usage in FieldLayout.)
3794  * @param {string} title Tooltip label for `title` attribute
3795  * @return {string}
3796  */
3797 OO.ui.mixin.AccessKeyedElement.prototype.formatTitleWithAccessKey = function ( title ) {
3798         if ( !this.$accessKeyed ) {
3799                 // Not initialized yet; the constructor will call updateTitle() which will rerun this
3800                 // function.
3801                 return title;
3802         }
3803         // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the
3804         // single key.
3805         let accessKey;
3806         if ( $.fn.updateTooltipAccessKeys && $.fn.updateTooltipAccessKeys.getAccessKeyLabel ) {
3807                 accessKey = $.fn.updateTooltipAccessKeys.getAccessKeyLabel( this.$accessKeyed[ 0 ] );
3808         } else {
3809                 accessKey = this.getAccessKey();
3810         }
3811         if ( accessKey ) {
3812                 title += ' [' + accessKey + ']';
3813         }
3814         return title;
3818  * RequiredElement is mixed into other classes to provide a `required` attribute.
3820  * @abstract
3821  * @class
3823  * @constructor
3824  * @param {Object} [config] Configuration options
3825  * @param {jQuery} [config.$required] The element to which the `required` attribute is applied.
3826  *  If this config is omitted, the required functionality is applied to $input if it
3827  *  exists, or $element if it doesn't.
3828  * @param {boolean} [config.required=false] Mark the field as required with `true`.
3829  * @param {OO.ui.Element} [config.indicatorElement=this] Element which mixes in OO.ui.mixin.IndicatorElement
3830  *  Will set the indicator on that element to 'required' when the element is required.
3831  *  Note that `false` & setting `indicator: 'required'` will result in no indicator shown.
3832  */
3833 OO.ui.mixin.RequiredElement = function OoUiMixinRequiredElement( config ) {
3834         // Configuration initialization
3835         config = config || {};
3837         // Properties
3838         this.$required = config.$required || this.$input || this.$element;
3839         this.required = false;
3840         this.indicatorElement = config.indicatorElement !== undefined ? config.indicatorElement : this;
3841         if ( this.indicatorElement && !this.indicatorElement.getIndicator ) {
3842                 throw new Error( 'config.indicatorElement must mixin OO.ui.mixin.IndicatorElement.' );
3843         }
3845         // Initialization
3846         this.setRequired( !!config.required );
3849 /* Setup */
3851 OO.initClass( OO.ui.mixin.RequiredElement );
3853 /* Methods */
3856  * Set the element which can take the required attribute.
3858  * This method is used to retarget a RequiredElement mixin so that its functionality applies to the
3859  * specified element.
3860  * If an element is already set, the mixin’s effect on that element is removed before the new
3861  * element is set up.
3863  * @param {jQuery} $required Element that should use the 'required' functionality
3864  */
3865 OO.ui.mixin.RequiredElement.prototype.setRequiredElement = function ( $required ) {
3866         if ( this.$required === $required ) {
3867                 return;
3868         }
3870         if ( this.$required && this.required ) {
3871                 this.updateRequiredElement( false );
3872         }
3874         this.$required = $required;
3875         this.updateRequiredElement();
3879  * @private
3880  * @param {boolean} [state]
3881  */
3882 OO.ui.mixin.RequiredElement.prototype.updateRequiredElement = function ( state ) {
3883         if ( state === undefined ) {
3884                 state = this.required;
3885         }
3887         this.$required
3888                 .prop( 'required', state );
3892  * Check if the input is {@link OO.ui.mixin.RequiredElement#required required}.
3894  * @return {boolean}
3895  */
3896 OO.ui.mixin.RequiredElement.prototype.isRequired = function () {
3897         return this.required;
3901  * Set the {@link OO.ui.mixin.RequiredElement#required required} state of the input.
3903  * @param {boolean} state Make input required
3904  * @chainable
3905  * @return {OO.ui.Widget} The widget, for chaining
3906  */
3907 OO.ui.mixin.RequiredElement.prototype.setRequired = function ( state ) {
3908         if ( this.required === state ) {
3909                 return this;
3910         }
3912         this.required = !!state;
3913         this.updateRequiredElement();
3914         if ( this.indicatorElement ) {
3915                 // Make sure to not destroy other, unrelated indicators
3916                 const expected = state ? null : 'required';
3917                 if ( this.indicatorElement.getIndicator() === expected ) {
3918                         this.indicatorElement.setIndicator( state ? 'required' : null );
3919                 }
3920         }
3921         return this;
3925  * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3926  * feels, and functionality can be customized via the class’s configuration options
3927  * and methods. Please see the [OOUI documentation on MediaWiki][1] for more information
3928  * and examples.
3930  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3932  * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3934  *     @example
3935  *     // A button widget.
3936  *     const button = new OO.ui.ButtonWidget( {
3937  *         label: 'Button with Icon',
3938  *         icon: 'trash',
3939  *         title: 'Remove'
3940  *     } );
3941  *     $( document.body ).append( button.$element );
3943  * @class
3944  * @extends OO.ui.Widget
3945  * @mixes OO.ui.mixin.ButtonElement
3946  * @mixes OO.ui.mixin.IconElement
3947  * @mixes OO.ui.mixin.IndicatorElement
3948  * @mixes OO.ui.mixin.LabelElement
3949  * @mixes OO.ui.mixin.TitledElement
3950  * @mixes OO.ui.mixin.FlaggedElement
3951  * @mixes OO.ui.mixin.TabIndexedElement
3952  * @mixes OO.ui.mixin.AccessKeyedElement
3954  * @constructor
3955  * @param {Object} [config] Configuration options
3956  * @param {boolean} [config.active=false] Whether button should be shown as active
3957  * @param {string} [config.href=null] Hyperlink to visit when the button is clicked.
3958  * @param {string} [config.target=null] The frame or window in which to open the hyperlink.
3959  * @param {boolean} [config.noFollow=true] Search engine traversal hint
3960  * @param {string|string[]} [config.rel=[]] Relationship attributes for the hyperlink
3961  */
3962 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
3963         // Configuration initialization
3964         config = config || {};
3966         // Parent constructor
3967         OO.ui.ButtonWidget.super.call( this, config );
3969         // Mixin constructors
3970         OO.ui.mixin.ButtonElement.call( this, config );
3971         OO.ui.mixin.IconElement.call( this, config );
3972         OO.ui.mixin.IndicatorElement.call( this, config );
3973         OO.ui.mixin.LabelElement.call( this, config );
3974         OO.ui.mixin.TitledElement.call( this, Object.assign( {
3975                 $titled: this.$button
3976         }, config ) );
3977         OO.ui.mixin.FlaggedElement.call( this, config );
3978         OO.ui.mixin.TabIndexedElement.call( this, Object.assign( {
3979                 $tabIndexed: this.$button
3980         }, config ) );
3981         OO.ui.mixin.AccessKeyedElement.call( this, Object.assign( {
3982                 $accessKeyed: this.$button
3983         }, config ) );
3985         // Properties
3986         this.href = null;
3987         this.target = null;
3988         this.noFollow = false;
3989         this.rel = [];
3991         // Events
3992         this.connect( this, {
3993                 disable: 'onDisable'
3994         } );
3996         // Initialization
3997         this.$button.append( this.$icon, this.$label, this.$indicator );
3998         this.$element
3999                 .addClass( 'oo-ui-buttonWidget' )
4000                 .append( this.$button );
4001         this.setActive( config.active );
4002         this.setHref( config.href );
4003         this.setTarget( config.target );
4004         if ( config.rel !== undefined ) {
4005                 this.setRel( config.rel );
4006         } else {
4007                 this.setNoFollow( config.noFollow );
4008         }
4011 /* Setup */
4013 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
4014 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
4015 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
4016 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
4017 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
4018 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
4019 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
4020 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
4021 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
4023 /* Static Properties */
4026  * @static
4027  * @inheritdoc
4028  */
4029 OO.ui.ButtonWidget.static.cancelButtonMouseDownEvents = false;
4032  * @static
4033  * @inheritdoc
4034  */
4035 OO.ui.ButtonWidget.static.tagName = 'span';
4037 /* Methods */
4040  * Get hyperlink location.
4042  * @return {string|null} Hyperlink location
4043  */
4044 OO.ui.ButtonWidget.prototype.getHref = function () {
4045         return this.href;
4049  * Get hyperlink target.
4051  * @return {string|null} Hyperlink target
4052  */
4053 OO.ui.ButtonWidget.prototype.getTarget = function () {
4054         return this.target;
4058  * Get search engine traversal hint.
4060  * @return {boolean} Whether search engines should avoid traversing this hyperlink
4061  */
4062 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
4063         return this.noFollow;
4067  * Get the relationship attribute of the hyperlink.
4069  * @return {string[]} Relationship attributes that apply to the hyperlink
4070  */
4071 OO.ui.ButtonWidget.prototype.getRel = function () {
4072         return this.rel;
4076  * Set hyperlink location.
4078  * @param {string|null} href Hyperlink location, null to remove
4079  * @chainable
4080  * @return {OO.ui.Widget} The widget, for chaining
4081  */
4082 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
4083         href = typeof href === 'string' ? href : null;
4084         if ( href !== null && !OO.ui.isSafeUrl( href ) ) {
4085                 href = './' + href;
4086         }
4088         if ( href !== this.href ) {
4089                 this.href = href;
4090                 this.updateHref();
4091         }
4093         return this;
4097  * Update the `href` attribute, in case of changes to href or
4098  * disabled state.
4100  * @private
4101  * @chainable
4102  * @return {OO.ui.Widget} The widget, for chaining
4103  */
4104 OO.ui.ButtonWidget.prototype.updateHref = function () {
4105         if ( this.href !== null && !this.isDisabled() ) {
4106                 this.$button.attr( 'href', this.href );
4107         } else {
4108                 this.$button.removeAttr( 'href' );
4109         }
4111         return this;
4115  * Handle disable events.
4117  * @private
4118  * @param {boolean} disabled Element is disabled
4119  */
4120 OO.ui.ButtonWidget.prototype.onDisable = function () {
4121         this.updateHref();
4125  * Set hyperlink target.
4127  * @param {string|null} target Hyperlink target, null to remove
4128  * @return {OO.ui.Widget} The widget, for chaining
4129  */
4130 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
4131         target = typeof target === 'string' ? target : null;
4133         if ( target !== this.target ) {
4134                 this.target = target;
4135                 if ( target !== null ) {
4136                         this.$button.attr( 'target', target );
4137                 } else {
4138                         this.$button.removeAttr( 'target' );
4139                 }
4140         }
4142         return this;
4146  * Set search engine traversal hint.
4148  * @param {boolean} [noFollow=true] True if search engines should avoid traversing this hyperlink
4149  * @return {OO.ui.Widget} The widget, for chaining
4150  */
4151 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
4152         noFollow = typeof noFollow === 'boolean' ? noFollow : true;
4154         if ( noFollow !== this.noFollow ) {
4155                 let rel;
4156                 if ( noFollow ) {
4157                         rel = this.rel.concat( [ 'nofollow' ] );
4158                 } else {
4159                         rel = this.rel.filter( ( value ) => value !== 'nofollow' );
4160                 }
4161                 this.setRel( rel );
4162         }
4164         return this;
4168  * Set the `rel` attribute of the hyperlink.
4170  * @param {string|string[]} [rel] Relationship attributes for the hyperlink, omit to remove
4171  * @return {OO.ui.Widget} The widget, for chaining
4172  */
4173 OO.ui.ButtonWidget.prototype.setRel = function ( rel ) {
4174         if ( !Array.isArray( rel ) ) {
4175                 rel = rel ? [ rel ] : [];
4176         }
4178         this.rel = rel;
4179         // For backwards compatibility.
4180         this.noFollow = rel.indexOf( 'nofollow' ) !== -1;
4181         this.$button.attr( 'rel', rel.join( ' ' ) || null );
4183         return this;
4186 // Override method visibility hints from ButtonElement
4188  * @method setActive
4189  * @inheritdoc OO.ui.mixin.ButtonElement
4190  * @memberof OO.ui.ButtonWidget#
4191  */
4193  * @method isActive
4194  * @inheritdoc OO.ui.mixin.ButtonElement
4195  * @memberof OO.ui.ButtonWidget#
4196  */
4199  * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
4200  * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
4201  * removed, and cleared from the group.
4203  *     @example
4204  *     // A ButtonGroupWidget with two buttons.
4205  *     const button1 = new OO.ui.PopupButtonWidget( {
4206  *             label: 'Select a category',
4207  *             icon: 'menu',
4208  *             popup: {
4209  *                 $content: $( '<p>List of categories…</p>' ),
4210  *                 padded: true,
4211  *                 align: 'left'
4212  *             }
4213  *         } ),
4214  *         button2 = new OO.ui.ButtonWidget( {
4215  *             label: 'Add item'
4216  *         } ),
4217  *         buttonGroup = new OO.ui.ButtonGroupWidget( {
4218  *             items: [ button1, button2 ]
4219  *         } );
4220  *     $( document.body ).append( buttonGroup.$element );
4222  * @class
4223  * @extends OO.ui.Widget
4224  * @mixes OO.ui.mixin.GroupElement
4225  * @mixes OO.ui.mixin.TitledElement
4227  * @constructor
4228  * @param {Object} [config] Configuration options
4229  * @param {OO.ui.ButtonWidget[]} [config.items] Buttons to add
4230  */
4231 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
4232         // Configuration initialization
4233         config = config || {};
4235         // Parent constructor
4236         OO.ui.ButtonGroupWidget.super.call( this, config );
4238         // Mixin constructors
4239         OO.ui.mixin.GroupElement.call( this, Object.assign( {
4240                 $group: this.$element
4241         }, config ) );
4242         OO.ui.mixin.TitledElement.call( this, config );
4244         // Initialization
4245         this.$element.addClass( 'oo-ui-buttonGroupWidget' );
4246         this.addItems( config.items || [] );
4249 /* Setup */
4251 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
4252 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
4253 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.TitledElement );
4255 /* Static Properties */
4258  * @static
4259  * @inheritdoc
4260  */
4261 OO.ui.ButtonGroupWidget.static.tagName = 'span';
4263 /* Methods */
4266  * Focus the widget
4268  * @chainable
4269  * @return {OO.ui.Widget} The widget, for chaining
4270  */
4271 OO.ui.ButtonGroupWidget.prototype.focus = function () {
4272         if ( !this.isDisabled() ) {
4273                 if ( this.items[ 0 ] ) {
4274                         this.items[ 0 ].focus();
4275                 }
4276         }
4277         return this;
4281  * @inheritdoc
4282  */
4283 OO.ui.ButtonGroupWidget.prototype.simulateLabelClick = function () {
4284         this.focus();
4288  * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}.
4289  * In general, IconWidgets should be used with OO.ui.LabelWidget, which creates a label that
4290  * identifies the icon’s function. See the [OOUI documentation on MediaWiki][1]
4291  * for a list of icons included in the library.
4293  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4295  *     @example
4296  *     // An IconWidget with a label via LabelWidget.
4297  *     const myIcon = new OO.ui.IconWidget( {
4298  *             icon: 'help',
4299  *             title: 'Help'
4300  *          } ),
4301  *          // Create a label.
4302  *          iconLabel = new OO.ui.LabelWidget( {
4303  *              label: 'Help'
4304  *          } );
4305  *      $( document.body ).append( myIcon.$element, iconLabel.$element );
4307  * @class
4308  * @extends OO.ui.Widget
4309  * @mixes OO.ui.mixin.IconElement
4310  * @mixes OO.ui.mixin.TitledElement
4311  * @mixes OO.ui.mixin.LabelElement
4312  * @mixes OO.ui.mixin.FlaggedElement
4314  * @constructor
4315  * @param {Object} [config] Configuration options
4316  */
4317 OO.ui.IconWidget = function OoUiIconWidget( config ) {
4318         // Configuration initialization
4319         config = config || {};
4321         // Parent constructor
4322         OO.ui.IconWidget.super.call( this, config );
4324         // Mixin constructors
4325         OO.ui.mixin.IconElement.call( this, Object.assign( {
4326                 $icon: this.$element
4327         }, config ) );
4328         OO.ui.mixin.TitledElement.call( this, Object.assign( {
4329                 $titled: this.$element
4330         }, config ) );
4331         OO.ui.mixin.LabelElement.call( this, Object.assign( {
4332                 $label: this.$element,
4333                 invisibleLabel: true
4334         }, config ) );
4335         OO.ui.mixin.FlaggedElement.call( this, Object.assign( {
4336                 $flagged: this.$element
4337         }, config ) );
4339         // Initialization
4340         this.$element.addClass( 'oo-ui-iconWidget' );
4341         // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4342         // nested in other widgets, because this widget used to not mix in LabelElement.
4343         this.$element.removeClass( 'oo-ui-labelElement-label' );
4346 /* Setup */
4348 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
4349 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
4350 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
4351 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.LabelElement );
4352 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
4354 /* Static Properties */
4357  * @static
4358  * @inheritdoc
4359  */
4360 OO.ui.IconWidget.static.tagName = 'span';
4363  * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4364  * attention to the status of an item or to clarify the function within a control. For a list of
4365  * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4367  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4369  *     @example
4370  *     // An indicator widget.
4371  *     const indicator1 = new OO.ui.IndicatorWidget( {
4372  *             indicator: 'required'
4373  *         } ),
4374  *         // Create a fieldset layout to add a label.
4375  *         fieldset = new OO.ui.FieldsetLayout();
4376  *     fieldset.addItems( [
4377  *         new OO.ui.FieldLayout( indicator1, {
4378  *             label: 'A required indicator:'
4379  *         } )
4380  *     ] );
4381  *     $( document.body ).append( fieldset.$element );
4383  * @class
4384  * @extends OO.ui.Widget
4385  * @mixes OO.ui.mixin.IndicatorElement
4386  * @mixes OO.ui.mixin.TitledElement
4387  * @mixes OO.ui.mixin.LabelElement
4389  * @constructor
4390  * @param {Object} [config] Configuration options
4391  */
4392 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
4393         // Configuration initialization
4394         config = config || {};
4396         // Parent constructor
4397         OO.ui.IndicatorWidget.super.call( this, config );
4399         // Mixin constructors
4400         OO.ui.mixin.IndicatorElement.call( this, Object.assign( {
4401                 $indicator: this.$element
4402         }, config ) );
4403         OO.ui.mixin.TitledElement.call( this, Object.assign( {
4404                 $titled: this.$element
4405         }, config ) );
4406         OO.ui.mixin.LabelElement.call( this, Object.assign( {
4407                 $label: this.$element,
4408                 invisibleLabel: true
4409         }, config ) );
4411         // Initialization
4412         this.$element.addClass( 'oo-ui-indicatorWidget' );
4413         // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4414         // nested in other widgets, because this widget used to not mix in LabelElement.
4415         this.$element.removeClass( 'oo-ui-labelElement-label' );
4418 /* Setup */
4420 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
4421 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
4422 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
4423 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.LabelElement );
4425 /* Static Properties */
4428  * @static
4429  * @inheritdoc
4430  */
4431 OO.ui.IndicatorWidget.static.tagName = 'span';
4434  * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4435  * be configured with a `label` option that is set to a string, a label node, or a function:
4437  * - String: a plaintext string
4438  * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4439  *   label that includes a link or special styling, such as a gray color or additional
4440  *   graphical elements.
4441  * - Function: a function that will produce a string in the future. Functions are used
4442  *   in cases where the value of the label is not currently defined.
4444  * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget},
4445  * which will come into focus when the label is clicked.
4447  *     @example
4448  *     // Two LabelWidgets.
4449  *     const label1 = new OO.ui.LabelWidget( {
4450  *             label: 'plaintext label'
4451  *         } ),
4452  *         label2 = new OO.ui.LabelWidget( {
4453  *             label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4454  *         } ),
4455  *         // Create a fieldset layout with fields for each example.
4456  *         fieldset = new OO.ui.FieldsetLayout();
4457  *     fieldset.addItems( [
4458  *         new OO.ui.FieldLayout( label1 ),
4459  *         new OO.ui.FieldLayout( label2 )
4460  *     ] );
4461  *     $( document.body ).append( fieldset.$element );
4463  * @class
4464  * @extends OO.ui.Widget
4465  * @mixes OO.ui.mixin.LabelElement
4466  * @mixes OO.ui.mixin.TitledElement
4468  * @constructor
4469  * @param {Object} [config] Configuration options
4470  * @param {OO.ui.InputWidget} [config.input] {@link OO.ui.InputWidget Input widget} that uses the label.
4471  *  Clicking the label will focus the specified input field.
4472  */
4473 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
4474         // Configuration initialization
4475         config = config || {};
4477         // Parent constructor
4478         OO.ui.LabelWidget.super.call( this, config );
4480         // Mixin constructors
4481         OO.ui.mixin.LabelElement.call( this, Object.assign( {
4482                 $label: this.$element
4483         }, config ) );
4484         OO.ui.mixin.TitledElement.call( this, config );
4486         // Properties
4487         this.input = config.input;
4489         // Initialization
4490         if ( this.input ) {
4491                 if ( this.input.getInputId() ) {
4492                         this.$element.attr( 'for', this.input.getInputId() );
4493                 } else {
4494                         this.$label.on( 'click', () => {
4495                                 this.input.simulateLabelClick();
4496                         } );
4497                 }
4498         }
4499         this.$element.addClass( 'oo-ui-labelWidget' );
4502 /* Setup */
4504 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
4505 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
4506 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
4508 /* Static Properties */
4511  * @static
4512  * @inheritdoc
4513  */
4514 OO.ui.LabelWidget.static.tagName = 'label';
4517  * MessageWidget produces a visual component for sending a notice to the user
4518  * with an icon and distinct design noting its purpose. The MessageWidget changes
4519  * its visual presentation based on the type chosen, which also denotes its UX
4520  * purpose.
4522  * @class
4523  * @extends OO.ui.Widget
4524  * @mixes OO.ui.mixin.IconElement
4525  * @mixes OO.ui.mixin.LabelElement
4526  * @mixes OO.ui.mixin.TitledElement
4527  * @mixes OO.ui.mixin.FlaggedElement
4529  * @constructor
4530  * @param {Object} [config] Configuration options
4531  * @param {string} [config.type='notice'] The type of the notice widget. This will also
4532  *  impact the flags that the widget receives (and hence its CSS design) as well
4533  *  as the icon that appears. Available types:
4534  *  'notice', 'error', 'warning', 'success'
4535  * @param {boolean} [config.inline=false] Set the notice as an inline notice. The default
4536  *  is not inline, or 'boxed' style.
4537  * @param {boolean} [config.showClose] Show a close button. Can't be used with inline.
4538  */
4539 OO.ui.MessageWidget = function OoUiMessageWidget( config ) {
4540         // Configuration initialization
4541         config = config || {};
4543         // Parent constructor
4544         OO.ui.MessageWidget.super.call( this, config );
4546         // Mixin constructors
4547         OO.ui.mixin.IconElement.call( this, config );
4548         OO.ui.mixin.LabelElement.call( this, config );
4549         OO.ui.mixin.TitledElement.call( this, config );
4550         OO.ui.mixin.FlaggedElement.call( this, config );
4552         // Set type
4553         this.setType( config.type );
4554         this.setInline( config.inline );
4556         // If an icon is passed in, set it again as setType will
4557         // have overridden the setIcon call in the IconElement constructor
4558         if ( config.icon ) {
4559                 this.setIcon( config.icon );
4560         }
4562         if ( !this.inline && config.showClose ) {
4563                 this.closeButton = new OO.ui.ButtonWidget( {
4564                         classes: [ 'oo-ui-messageWidget-close' ],
4565                         framed: false,
4566                         icon: 'close',
4567                         label: OO.ui.msg( 'ooui-popup-widget-close-button-aria-label' ),
4568                         invisibleLabel: true
4569                 } );
4570                 this.closeButton.connect( this, {
4571                         click: 'onCloseButtonClick'
4572                 } );
4573                 this.$element.addClass( 'oo-ui-messageWidget-showClose' );
4574         }
4576         // Build the widget
4577         this.$element
4578                 .append( this.$icon, this.$label, this.closeButton && this.closeButton.$element )
4579                 .addClass( 'oo-ui-messageWidget' );
4582 /* Setup */
4584 OO.inheritClass( OO.ui.MessageWidget, OO.ui.Widget );
4585 OO.mixinClass( OO.ui.MessageWidget, OO.ui.mixin.IconElement );
4586 OO.mixinClass( OO.ui.MessageWidget, OO.ui.mixin.LabelElement );
4587 OO.mixinClass( OO.ui.MessageWidget, OO.ui.mixin.TitledElement );
4588 OO.mixinClass( OO.ui.MessageWidget, OO.ui.mixin.FlaggedElement );
4590 /* Events */
4593  * @event OO.ui.MessageWidget#close
4594  */
4596 /* Static Properties */
4599  * An object defining the icon name per defined type.
4601  * @static
4602  * @property {Object}
4603  */
4604 OO.ui.MessageWidget.static.iconMap = {
4605         notice: 'infoFilled',
4606         error: 'error',
4607         warning: 'alert',
4608         success: 'success'
4611 /* Methods */
4614  * Set the inline state of the widget.
4616  * @param {boolean} [inline=false] Widget is inline
4617  */
4618 OO.ui.MessageWidget.prototype.setInline = function ( inline ) {
4619         inline = !!inline;
4621         if ( this.inline !== inline ) {
4622                 this.inline = inline;
4623                 this.$element
4624                         .toggleClass( 'oo-ui-messageWidget-block', !this.inline );
4625         }
4628  * Set the widget type. The given type must belong to the list of
4629  * legal types set by OO.ui.MessageWidget.static.iconMap
4631  * @param {string} [type='notice']
4632  */
4633 OO.ui.MessageWidget.prototype.setType = function ( type ) {
4634         if ( !this.constructor.static.iconMap[ type ] ) {
4635                 type = 'notice';
4636         }
4638         if ( this.type !== type ) {
4639                 // Flags
4640                 this.clearFlags();
4641                 this.setFlags( type );
4643                 // Set the icon and its variant
4644                 this.setIcon( this.constructor.static.iconMap[ type ] );
4645                 this.$icon.removeClass( 'oo-ui-image-' + this.type );
4646                 this.$icon.addClass( 'oo-ui-image-' + type );
4648                 if ( type === 'error' ) {
4649                         this.$element.attr( 'role', 'alert' );
4650                         this.$element.removeAttr( 'aria-live' );
4651                 } else {
4652                         this.$element.removeAttr( 'role' );
4653                         this.$element.attr( 'aria-live', 'polite' );
4654                 }
4656                 this.type = type;
4657         }
4661  * Handle click events on the close button
4663  * @param {jQuery} e jQuery event
4664  * @fires OO.ui.MessageWidget#close
4665  */
4666 OO.ui.MessageWidget.prototype.onCloseButtonClick = function () {
4667         this.toggle( false );
4668         this.emit( 'close' );
4672  * ToggleWidget implements basic behavior of widgets with an on/off state.
4673  * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
4675  * @abstract
4676  * @class
4677  * @extends OO.ui.Widget
4678  * @mixes OO.ui.mixin.TitledElement
4680  * @constructor
4681  * @param {Object} [config] Configuration options
4682  * @param {boolean} [config.value=false] The toggle’s initial on/off state.
4683  *  By default, the toggle is in the 'off' state.
4684  */
4685 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
4686         // Configuration initialization
4687         config = config || {};
4689         // Parent constructor
4690         OO.ui.ToggleWidget.super.call( this, config );
4692         // Mixin constructor
4693         OO.ui.mixin.TitledElement.call( this, config );
4695         // Properties
4696         this.value = null;
4698         // Initialization
4699         this.$element.addClass( 'oo-ui-toggleWidget' );
4700         this.setValue( !!config.value );
4703 /* Setup */
4705 OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
4706 OO.mixinClass( OO.ui.ToggleWidget, OO.ui.mixin.TitledElement );
4708 /* Events */
4711  * A change event is emitted when the on/off state of the toggle changes.
4713  * @event OO.ui.ToggleWidget#change
4714  * @param {boolean} value Value representing the new state of the toggle
4715  */
4717 /* Methods */
4720  * Get the value representing the toggle’s state.
4722  * @return {boolean} The on/off state of the toggle
4723  */
4724 OO.ui.ToggleWidget.prototype.getValue = function () {
4725         return this.value;
4729  * Set the state of the toggle: `true` for 'on', `false` for 'off'.
4731  * @param {boolean} [value=false] The state of the toggle
4732  * @fires OO.ui.ToggleWidget#change
4733  * @chainable
4734  * @return {OO.ui.Widget} The widget, for chaining
4735  */
4736 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
4737         value = !!value;
4738         if ( this.value !== value ) {
4739                 this.value = value;
4740                 this.emit( 'change', value );
4741                 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
4742                 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
4743         }
4744         return this;
4748  * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
4749  * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
4750  * visually by a slider in the leftmost position.
4752  *     @example
4753  *     // Toggle switches in the 'off' and 'on' position.
4754  *     const toggleSwitch1 = new OO.ui.ToggleSwitchWidget(),
4755  *         toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
4756  *             value: true
4757  *         } );
4758  *         // Create a FieldsetLayout to layout and label switches.
4759  *         fieldset = new OO.ui.FieldsetLayout( {
4760  *             label: 'Toggle switches'
4761  *         } );
4762  *     fieldset.addItems( [
4763  *         new OO.ui.FieldLayout( toggleSwitch1, {
4764  *             label: 'Off',
4765  *             align: 'top'
4766  *         } ),
4767  *         new OO.ui.FieldLayout( toggleSwitch2, {
4768  *             label: 'On',
4769  *             align: 'top'
4770  *         } )
4771  *     ] );
4772  *     $( document.body ).append( fieldset.$element );
4774  * @class
4775  * @extends OO.ui.ToggleWidget
4776  * @mixes OO.ui.mixin.TabIndexedElement
4778  * @constructor
4779  * @param {Object} [config] Configuration options
4780  * @param {boolean} [config.value=false] The toggle switch’s initial on/off state.
4781  *  By default, the toggle switch is in the 'off' position.
4782  */
4783 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
4784         // Parent constructor
4785         OO.ui.ToggleSwitchWidget.super.call( this, config );
4787         // Mixin constructors
4788         OO.ui.mixin.TabIndexedElement.call( this, config );
4790         // Properties
4791         this.dragging = false;
4792         this.dragStart = null;
4793         this.sliding = false;
4794         this.$glow = $( '<span>' );
4795         this.$grip = $( '<span>' );
4797         // Events
4798         this.$element.on( {
4799                 click: this.onClick.bind( this ),
4800                 keypress: this.onKeyPress.bind( this )
4801         } );
4803         // Initialization
4804         this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
4805         this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
4806         this.$element
4807                 .addClass( 'oo-ui-toggleSwitchWidget' )
4808                 .attr( 'role', 'switch' )
4809                 .append( this.$glow, this.$grip );
4812 /* Setup */
4814 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
4815 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
4817 /* Methods */
4820  * Handle mouse click events.
4822  * @private
4823  * @param {jQuery.Event} e Mouse click event
4824  * @return {undefined|boolean} False to prevent default if event is handled
4825  */
4826 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
4827         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
4828                 this.setValue( !this.value );
4829         }
4830         return false;
4834  * Handle key press events.
4836  * @private
4837  * @param {jQuery.Event} e Key press event
4838  * @return {undefined|boolean} False to prevent default if event is handled
4839  */
4840 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
4841         if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
4842                 this.setValue( !this.value );
4843                 return false;
4844         }
4848  * @inheritdoc
4849  */
4850 OO.ui.ToggleSwitchWidget.prototype.setValue = function ( value ) {
4851         OO.ui.ToggleSwitchWidget.super.prototype.setValue.call( this, value );
4852         this.$element.attr( 'aria-checked', this.value.toString() );
4853         return this;
4857  * @inheritdoc
4858  */
4859 OO.ui.ToggleSwitchWidget.prototype.simulateLabelClick = function () {
4860         if ( !this.isDisabled() ) {
4861                 this.setValue( !this.value );
4862         }
4863         this.focus();
4867  * PendingElement is a mixin that is used to create elements that notify users that something is
4868  * happening and that they should wait before proceeding. The pending state is visually represented
4869  * with a pending texture that appears in the head of a pending
4870  * {@link OO.ui.ProcessDialog process dialog} or in the input field of a
4871  * {@link OO.ui.TextInputWidget text input widget}.
4873  * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked
4874  * as pending, but only when used in {@link OO.ui.MessageDialog message dialogs}. The behavior is
4875  * not currently supported for action widgets used in process dialogs.
4877  *     @example
4878  *     function MessageDialog( config ) {
4879  *         MessageDialog.super.call( this, config );
4880  *     }
4881  *     OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4883  *     MessageDialog.static.name = 'myMessageDialog';
4884  *     MessageDialog.static.actions = [
4885  *         { action: 'save', label: 'Done', flags: 'primary' },
4886  *         { label: 'Cancel', flags: 'safe' }
4887  *     ];
4889  *     MessageDialog.prototype.initialize = function () {
4890  *         MessageDialog.super.prototype.initialize.apply( this, arguments );
4891  *         this.content = new OO.ui.PanelLayout( { padded: true } );
4892  *         this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending ' +
4893  *             'state. Note that action widgets can be marked pending in message dialogs but not ' +
4894  *             'process dialogs.</p>' );
4895  *         this.$body.append( this.content.$element );
4896  *     };
4897  *     MessageDialog.prototype.getBodyHeight = function () {
4898  *         return 100;
4899  *     }
4900  *     MessageDialog.prototype.getActionProcess = function ( action ) {
4901  *         if ( action === 'save' ) {
4902  *             this.getActions().get({actions: 'save'})[0].pushPending();
4903  *             return new OO.ui.Process()
4904  *                 .next( 1000 )
4905  *                 .next( () => {
4906  *                     this.getActions().get({actions: 'save'})[0].popPending();
4907  *                 } );
4908  *         }
4909  *         return MessageDialog.super.prototype.getActionProcess.call( this, action );
4910  *     };
4912  *     const windowManager = new OO.ui.WindowManager();
4913  *     $( document.body ).append( windowManager.$element );
4915  *     const dialog = new MessageDialog();
4916  *     windowManager.addWindows( [ dialog ] );
4917  *     windowManager.openWindow( dialog );
4919  * @abstract
4920  * @class
4922  * @constructor
4923  * @param {Object} [config] Configuration options
4924  * @param {jQuery} [config.$pending] Element to mark as pending, defaults to this.$element
4925  */
4926 OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
4927         // Configuration initialization
4928         config = config || {};
4930         // Properties
4931         this.pending = 0;
4932         this.$pending = null;
4934         // Initialisation
4935         this.setPendingElement( config.$pending || this.$element );
4938 /* Setup */
4940 OO.initClass( OO.ui.mixin.PendingElement );
4942 /* Methods */
4945  * Set the pending element (and clean up any existing one).
4947  * @param {jQuery} $pending The element to set to pending.
4948  */
4949 OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
4950         if ( this.$pending ) {
4951                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
4952         }
4954         this.$pending = $pending;
4955         if ( this.pending > 0 ) {
4956                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
4957         }
4961  * Check if an element is pending.
4963  * @return {boolean} Element is pending
4964  */
4965 OO.ui.mixin.PendingElement.prototype.isPending = function () {
4966         return !!this.pending;
4970  * Increase the pending counter. The pending state will remain active until the counter is zero
4971  * (i.e., the number of calls to #pushPending and #popPending is the same).
4973  * @chainable
4974  * @return {OO.ui.Element} The element, for chaining
4975  */
4976 OO.ui.mixin.PendingElement.prototype.pushPending = function () {
4977         if ( this.pending === 0 ) {
4978                 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
4979                 this.updateThemeClasses();
4980         }
4981         this.pending++;
4983         return this;
4987  * Decrease the pending counter. The pending state will remain active until the counter is zero
4988  * (i.e., the number of calls to #pushPending and #popPending is the same).
4990  * @chainable
4991  * @return {OO.ui.Element} The element, for chaining
4992  */
4993 OO.ui.mixin.PendingElement.prototype.popPending = function () {
4994         if ( this.pending === 1 ) {
4995                 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
4996                 this.updateThemeClasses();
4997         }
4998         this.pending = Math.max( 0, this.pending - 1 );
5000         return this;
5004  * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
5005  * in the document (for example, in an OO.ui.Window's $overlay).
5007  * The elements's position is automatically calculated and maintained when window is resized or the
5008  * page is scrolled. If you reposition the container manually, you have to call #position to make
5009  * sure the element is still placed correctly.
5011  * As positioning is only possible when both the element and the container are attached to the DOM
5012  * and visible, it's only done after you call #togglePositioning. You might want to do this inside
5013  * the #toggle method to display a floating popup, for example.
5015  * @abstract
5016  * @class
5018  * @constructor
5019  * @param {Object} [config] Configuration options
5020  * @param {jQuery} [config.$floatable] Node to position, assigned to #$floatable, omit to use #$element
5021  * @param {jQuery} [config.$floatableContainer] Node to position adjacent to
5022  * @param {string} [config.verticalPosition='below'] Where to position $floatable vertically:
5023  *  'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
5024  *  'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
5025  *  'top': Align the top edge with $floatableContainer's top edge
5026  *  'bottom': Align the bottom edge with $floatableContainer's bottom edge
5027  *  'center': Vertically align the center with $floatableContainer's center
5028  * @param {string} [config.horizontalPosition='start'] Where to position $floatable horizontally:
5029  *  'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
5030  *  'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
5031  *  'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
5032  *  'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
5033  *  'center': Horizontally align the center with $floatableContainer's center
5034  * @param {boolean} [config.hideWhenOutOfView=true] Whether to hide the floatable element if the
5035  *   container is out of view
5036  * @param {number} [config.spacing=0] Spacing from $floatableContainer, when $floatable is
5037  *  positioned outside the container (i.e. below/above/before/after).
5038  */
5039 OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
5040         // Configuration initialization
5041         config = config || {};
5043         // Properties
5044         this.$floatable = null;
5045         this.$floatableContainer = null;
5046         this.$floatableWindow = null;
5047         this.$floatableClosestScrollable = null;
5048         this.floatableOutOfView = false;
5049         this.onFloatableScrollHandler = this.position.bind( this );
5050         this.onFloatableWindowResizeHandler = this.position.bind( this );
5052         // Initialization
5053         this.setFloatableContainer( config.$floatableContainer );
5054         this.setFloatableElement( config.$floatable || this.$element );
5055         this.setVerticalPosition( config.verticalPosition || 'below' );
5056         this.setHorizontalPosition( config.horizontalPosition || 'start' );
5057         this.spacing = config.spacing || 0;
5058         this.hideWhenOutOfView = config.hideWhenOutOfView === undefined ?
5059                 true : !!config.hideWhenOutOfView;
5062 /* Methods */
5065  * Set floatable element.
5067  * If an element is already set, it will be cleaned up before setting up the new element.
5069  * @param {jQuery} $floatable Element to make floatable
5070  */
5071 OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
5072         if ( this.$floatable ) {
5073                 this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
5074                 this.$floatable.css( { top: '', left: '', bottom: '', right: '' } );
5075         }
5077         this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
5078         this.position();
5082  * Set floatable container.
5084  * The element will be positioned relative to the specified container.
5086  * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
5087  */
5088 OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
5089         this.$floatableContainer = $floatableContainer;
5090         if ( this.$floatable ) {
5091                 this.position();
5092         }
5096  * Change how the element is positioned vertically.
5098  * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
5099  */
5100 OO.ui.mixin.FloatableElement.prototype.setVerticalPosition = function ( position ) {
5101         if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position ) === -1 ) {
5102                 throw new Error( 'Invalid value for vertical position: ' + position );
5103         }
5104         if ( this.verticalPosition !== position ) {
5105                 this.verticalPosition = position;
5106                 if ( this.$floatable ) {
5107                         this.position();
5108                 }
5109         }
5113  * Change how the element is positioned horizontally.
5115  * @param {string} position 'before', 'after', 'start', 'end' or 'center'
5116  */
5117 OO.ui.mixin.FloatableElement.prototype.setHorizontalPosition = function ( position ) {
5118         if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position ) === -1 ) {
5119                 throw new Error( 'Invalid value for horizontal position: ' + position );
5120         }
5121         if ( this.horizontalPosition !== position ) {
5122                 this.horizontalPosition = position;
5123                 if ( this.$floatable ) {
5124                         this.position();
5125                 }
5126         }
5130  * Toggle positioning.
5132  * Do not turn positioning on until after the element is attached to the DOM and visible.
5134  * @param {boolean} [positioning] Enable positioning, omit to toggle
5135  * @chainable
5136  * @return {OO.ui.Element} The element, for chaining
5137  */
5138 OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
5139         if ( !this.$floatable || !this.$floatableContainer ) {
5140                 return this;
5141         }
5143         positioning = positioning === undefined ? !this.positioning : !!positioning;
5145         if ( positioning && !this.warnedUnattached && !this.isElementAttached() ) {
5146                 OO.ui.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
5147                 this.warnedUnattached = true;
5148         }
5150         if ( this.positioning !== positioning ) {
5151                 this.positioning = positioning;
5153                 let closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer(
5154                         this.$floatableContainer[ 0 ]
5155                 );
5156                 // If the scrollable is the root, we have to listen to scroll events
5157                 // on the window because of browser inconsistencies.
5158                 if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
5159                         closestScrollableOfContainer = OO.ui.Element.static.getWindow(
5160                                 closestScrollableOfContainer
5161                         );
5162                 }
5164                 if ( positioning ) {
5165                         this.$floatableWindow = $( this.getElementWindow() );
5166                         this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
5168                         this.$floatableClosestScrollable = $( closestScrollableOfContainer );
5169                         this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
5171                         // Initial position after visible
5172                         this.position();
5173                 } else {
5174                         if ( this.$floatableWindow ) {
5175                                 this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
5176                                 this.$floatableWindow = null;
5177                         }
5179                         if ( this.$floatableClosestScrollable ) {
5180                                 this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
5181                                 this.$floatableClosestScrollable = null;
5182                         }
5184                         this.$floatable.css( { top: '', left: '', bottom: '', right: '' } );
5185                 }
5186         }
5188         return this;
5192  * Check whether the bottom edge of the given element is within the viewport of the given
5193  * container.
5195  * @private
5196  * @param {jQuery} $element
5197  * @param {jQuery} $container
5198  * @return {boolean}
5199  */
5200 OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
5201         const direction = $element.css( 'direction' );
5203         const elemRect = $element[ 0 ].getBoundingClientRect();
5204         let contRect;
5205         if ( $container[ 0 ] === window ) {
5206                 const viewportSpacing = OO.ui.getViewportSpacing();
5207                 contRect = {
5208                         top: 0,
5209                         left: 0,
5210                         right: document.documentElement.clientWidth,
5211                         bottom: document.documentElement.clientHeight
5212                 };
5213                 contRect.top += viewportSpacing.top;
5214                 contRect.left += viewportSpacing.left;
5215                 contRect.right -= viewportSpacing.right;
5216                 contRect.bottom -= viewportSpacing.bottom;
5217         } else {
5218                 contRect = $container[ 0 ].getBoundingClientRect();
5219         }
5221         const topEdgeInBounds = elemRect.top >= contRect.top && elemRect.top <= contRect.bottom;
5222         const bottomEdgeInBounds = elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom;
5223         const leftEdgeInBounds = elemRect.left >= contRect.left && elemRect.left <= contRect.right;
5224         const rightEdgeInBounds = elemRect.right >= contRect.left && elemRect.right <= contRect.right;
5225         let startEdgeInBounds, endEdgeInBounds;
5226         if ( direction === 'rtl' ) {
5227                 startEdgeInBounds = rightEdgeInBounds;
5228                 endEdgeInBounds = leftEdgeInBounds;
5229         } else {
5230                 startEdgeInBounds = leftEdgeInBounds;
5231                 endEdgeInBounds = rightEdgeInBounds;
5232         }
5234         if ( this.verticalPosition === 'below' && !bottomEdgeInBounds ) {
5235                 return false;
5236         }
5237         if ( this.verticalPosition === 'above' && !topEdgeInBounds ) {
5238                 return false;
5239         }
5240         if ( this.horizontalPosition === 'before' && !startEdgeInBounds ) {
5241                 return false;
5242         }
5243         if ( this.horizontalPosition === 'after' && !endEdgeInBounds ) {
5244                 return false;
5245         }
5247         // The other positioning values are all about being inside the container,
5248         // so in those cases all we care about is that any part of the container is visible.
5249         return elemRect.top <= contRect.bottom && elemRect.bottom >= contRect.top &&
5250                 elemRect.left <= contRect.right && elemRect.right >= contRect.left;
5254  * Check if the floatable is hidden to the user because it was offscreen.
5256  * @return {boolean} Floatable is out of view
5257  */
5258 OO.ui.mixin.FloatableElement.prototype.isFloatableOutOfView = function () {
5259         return this.floatableOutOfView;
5263  * Position the floatable below its container.
5265  * This should only be done when both of them are attached to the DOM and visible.
5267  * @chainable
5268  * @return {OO.ui.Element} The element, for chaining
5269  */
5270 OO.ui.mixin.FloatableElement.prototype.position = function () {
5271         if ( !this.positioning ) {
5272                 return this;
5273         }
5275         if ( !(
5276                 // To continue, some things need to be true:
5277                 // The element must actually be in the DOM
5278                 this.isElementAttached() && (
5279                         // The closest scrollable is the current window
5280                         this.$floatableClosestScrollable[ 0 ] === this.getElementWindow() ||
5281                         // OR is an element in the element's DOM
5282                         $.contains( this.getElementDocument(), this.$floatableClosestScrollable[ 0 ] )
5283                 )
5284         ) ) {
5285                 // Abort early if important parts of the widget are no longer attached to the DOM
5286                 return this;
5287         }
5289         this.floatableOutOfView = this.hideWhenOutOfView &&
5290                 !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable );
5291         this.$floatable.toggleClass( 'oo-ui-element-hidden', this.floatableOutOfView );
5292         if ( this.floatableOutOfView ) {
5293                 return this;
5294         }
5296         this.$floatable.css( this.computePosition() );
5298         // We updated the position, so re-evaluate the clipping state.
5299         // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
5300         // will not notice the need to update itself.)
5301         // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here.
5302         // Why does it not listen to the right events in the right places?
5303         if ( this.clip ) {
5304                 this.clip();
5305         }
5307         return this;
5311  * Compute how #$floatable should be positioned based on the position of #$floatableContainer
5312  * and the positioning settings. This is a helper for #position that shouldn't be called directly,
5313  * but may be overridden by subclasses if they want to change or add to the positioning logic.
5315  * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
5316  */
5317 OO.ui.mixin.FloatableElement.prototype.computePosition = function () {
5318         const newPos = { top: '', left: '', bottom: '', right: '' };
5319         const direction = this.$floatableContainer.css( 'direction' );
5321         let $offsetParent = this.$floatable.offsetParent();
5323         if ( $offsetParent.is( 'html' ) ) {
5324                 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
5325                 // <html> element, but they do work on the <body>
5326                 $offsetParent = $( $offsetParent[ 0 ].ownerDocument.body );
5327         }
5328         const isBody = $offsetParent.is( 'body' );
5329         const scrollableX = $offsetParent.css( 'overflow-x' ) === 'scroll' ||
5330                 $offsetParent.css( 'overflow-x' ) === 'auto';
5331         const scrollableY = $offsetParent.css( 'overflow-y' ) === 'scroll' ||
5332                 $offsetParent.css( 'overflow-y' ) === 'auto';
5334         const vertScrollbarWidth = $offsetParent.innerWidth() - $offsetParent.prop( 'clientWidth' );
5335         const horizScrollbarHeight = $offsetParent.innerHeight() - $offsetParent.prop( 'clientHeight' );
5336         // We don't need to compute and add scrollTop and scrollLeft if the scrollable container
5337         // is the body, or if it isn't scrollable
5338         const scrollTop = scrollableY && !isBody ?
5339                 $offsetParent.scrollTop() : 0;
5340         const scrollLeft = scrollableX && !isBody ?
5341                 OO.ui.Element.static.getScrollLeft( $offsetParent[ 0 ] ) : 0;
5343         // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
5344         // if the <body> has a margin
5345         const containerPos = isBody ?
5346                 this.$floatableContainer.offset() :
5347                 OO.ui.Element.static.getRelativePosition( this.$floatableContainer, $offsetParent );
5348         containerPos.bottom = containerPos.top + this.$floatableContainer.outerHeight();
5349         containerPos.right = containerPos.left + this.$floatableContainer.outerWidth();
5350         containerPos.start = direction === 'rtl' ? containerPos.right : containerPos.left;
5351         containerPos.end = direction === 'rtl' ? containerPos.left : containerPos.right;
5353         if ( this.verticalPosition === 'below' ) {
5354                 newPos.top = containerPos.bottom + this.spacing;
5355         } else if ( this.verticalPosition === 'above' ) {
5356                 newPos.bottom = $offsetParent.outerHeight() - containerPos.top + this.spacing;
5357         } else if ( this.verticalPosition === 'top' ) {
5358                 newPos.top = containerPos.top;
5359         } else if ( this.verticalPosition === 'bottom' ) {
5360                 newPos.bottom = $offsetParent.outerHeight() - containerPos.bottom;
5361         } else if ( this.verticalPosition === 'center' ) {
5362                 newPos.top = containerPos.top +
5363                         ( this.$floatableContainer.height() - this.$floatable.height() ) / 2;
5364         }
5366         if ( this.horizontalPosition === 'before' ) {
5367                 newPos.end = containerPos.start - this.spacing;
5368         } else if ( this.horizontalPosition === 'after' ) {
5369                 newPos.start = containerPos.end + this.spacing;
5370         } else if ( this.horizontalPosition === 'start' ) {
5371                 newPos.start = containerPos.start;
5372         } else if ( this.horizontalPosition === 'end' ) {
5373                 newPos.end = containerPos.end;
5374         } else if ( this.horizontalPosition === 'center' ) {
5375                 newPos.left = containerPos.left +
5376                         ( this.$floatableContainer.width() - this.$floatable.width() ) / 2;
5377         }
5379         if ( newPos.start !== undefined ) {
5380                 if ( direction === 'rtl' ) {
5381                         newPos.right = ( isBody ? $( $offsetParent[ 0 ].ownerDocument.documentElement ) :
5382                                 $offsetParent ).outerWidth() - newPos.start;
5383                 } else {
5384                         newPos.left = newPos.start;
5385                 }
5386                 delete newPos.start;
5387         }
5388         if ( newPos.end !== undefined ) {
5389                 if ( direction === 'rtl' ) {
5390                         newPos.left = newPos.end;
5391                 } else {
5392                         newPos.right = ( isBody ? $( $offsetParent[ 0 ].ownerDocument.documentElement ) :
5393                                 $offsetParent ).outerWidth() - newPos.end;
5394                 }
5395                 delete newPos.end;
5396         }
5398         // Account for scroll position
5399         if ( newPos.top !== '' ) {
5400                 newPos.top += scrollTop;
5401         }
5402         if ( newPos.bottom !== '' ) {
5403                 newPos.bottom -= scrollTop;
5404         }
5405         if ( newPos.left !== '' ) {
5406                 newPos.left += scrollLeft;
5407         }
5408         if ( newPos.right !== '' ) {
5409                 newPos.right -= scrollLeft;
5410         }
5412         // Account for scrollbar gutter
5413         if ( newPos.bottom !== '' ) {
5414                 newPos.bottom -= horizScrollbarHeight;
5415         }
5416         if ( direction === 'rtl' ) {
5417                 if ( newPos.left !== '' ) {
5418                         newPos.left -= vertScrollbarWidth;
5419                 }
5420         } else {
5421                 if ( newPos.right !== '' ) {
5422                         newPos.right -= vertScrollbarWidth;
5423                 }
5424         }
5426         return newPos;
5430  * Element that can be automatically clipped to visible boundaries.
5432  * Whenever the element's natural height changes, you have to call
5433  * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
5434  * clipping correctly.
5436  * The dimensions of #$clippableContainer will be compared to the boundaries of the
5437  * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
5438  * then #$clippable will be given a fixed reduced height and/or width and will be made
5439  * scrollable. By default, #$clippable and #$clippableContainer are the same element,
5440  * but you can build a static footer by setting #$clippableContainer to an element that contains
5441  * #$clippable and the footer.
5443  * @abstract
5444  * @class
5446  * @constructor
5447  * @param {Object} [config] Configuration options
5448  * @param {jQuery} [config.$clippable] Node to clip, assigned to #$clippable, omit to use #$element
5449  * @param {jQuery} [config.$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
5450  *   omit to use #$clippable
5451  */
5452 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
5453         // Configuration initialization
5454         config = config || {};
5456         // Properties
5457         this.$clippable = null;
5458         this.$clippableContainer = null;
5459         this.clipping = false;
5460         this.clippedHorizontally = false;
5461         this.clippedVertically = false;
5462         this.$clippableScrollableContainer = null;
5463         this.$clippableScroller = null;
5464         this.$clippableWindow = null;
5465         this.idealWidth = null;
5466         this.idealHeight = null;
5467         this.onClippableScrollHandler = this.clip.bind( this );
5468         this.onClippableWindowResizeHandler = this.clip.bind( this );
5470         // Initialization
5471         if ( config.$clippableContainer ) {
5472                 this.setClippableContainer( config.$clippableContainer );
5473         }
5474         this.setClippableElement( config.$clippable || this.$element );
5477 /* Methods */
5480  * Set clippable element.
5482  * If an element is already set, it will be cleaned up before setting up the new element.
5484  * @param {jQuery} $clippable Element to make clippable
5485  */
5486 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
5487         if ( this.$clippable ) {
5488                 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
5489                 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
5490                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5491         }
5493         this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
5494         this.clip();
5498  * Set clippable container.
5500  * This is the container that will be measured when deciding whether to clip. When clipping,
5501  * #$clippable will be resized in order to keep the clippable container fully visible.
5503  * If the clippable container is unset, #$clippable will be used.
5505  * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
5506  */
5507 OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
5508         this.$clippableContainer = $clippableContainer;
5509         if ( this.$clippable ) {
5510                 this.clip();
5511         }
5515  * Toggle clipping.
5517  * Do not turn clipping on until after the element is attached to the DOM and visible.
5519  * @param {boolean} [clipping] Enable clipping, omit to toggle
5520  * @chainable
5521  * @return {OO.ui.Element} The element, for chaining
5522  */
5523 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
5524         clipping = clipping === undefined ? !this.clipping : !!clipping;
5526         if ( clipping && !this.warnedUnattached && !this.isElementAttached() ) {
5527                 OO.ui.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
5528                 this.warnedUnattached = true;
5529         }
5531         if ( this.clipping !== clipping ) {
5532                 this.clipping = clipping;
5533                 if ( clipping ) {
5534                         this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
5535                         // If the clippable container is the root, we have to listen to scroll events and check
5536                         // jQuery.scrollTop on the window because of browser inconsistencies
5537                         this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
5538                                 $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
5539                                 this.$clippableScrollableContainer;
5540                         this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
5541                         this.$clippableWindow = $( this.getElementWindow() )
5542                                 .on( 'resize', this.onClippableWindowResizeHandler );
5543                         // Initial clip after visible
5544                         this.clip();
5545                 } else {
5546                         this.$clippable.css( {
5547                                 width: '',
5548                                 height: '',
5549                                 maxWidth: '',
5550                                 maxHeight: '',
5551                                 overflowX: '',
5552                                 overflowY: ''
5553                         } );
5554                         OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5556                         this.$clippableScrollableContainer = null;
5557                         this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
5558                         this.$clippableScroller = null;
5559                         this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
5560                         this.$clippableWindow = null;
5561                 }
5562         }
5564         return this;
5568  * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5570  * @return {boolean} Element will be clipped to the visible area
5571  */
5572 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
5573         return this.clipping;
5577  * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5579  * @return {boolean} Part of the element is being clipped
5580  */
5581 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
5582         return this.clippedHorizontally || this.clippedVertically;
5586  * Check if the right of the element is being clipped by the nearest scrollable container.
5588  * @return {boolean} Part of the element is being clipped
5589  */
5590 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
5591         return this.clippedHorizontally;
5595  * Check if the bottom of the element is being clipped by the nearest scrollable container.
5597  * @return {boolean} Part of the element is being clipped
5598  */
5599 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
5600         return this.clippedVertically;
5604  * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
5606  * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
5607  * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
5608  */
5609 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
5610         this.idealWidth = width;
5611         this.idealHeight = height;
5613         if ( !this.clipping ) {
5614                 // Update dimensions
5615                 this.$clippable.css( { width: width, height: height } );
5616         }
5617         // While clipping, idealWidth and idealHeight are not considered
5621  * Return the side of the clippable on which it is "anchored" (aligned to something else).
5622  * ClippableElement will clip the opposite side when reducing element's width.
5624  * Classes that mix in ClippableElement should override this to return 'right' if their
5625  * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
5626  * If your class also mixes in FloatableElement, this is handled automatically.
5628  * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5629  * always in pixels, even if they were unset or set to 'auto'.)
5631  * When in doubt, 'left' (or 'right' in RTL) is a reasonable fallback.
5633  * @return {string} 'left' or 'right'
5634  */
5635 OO.ui.mixin.ClippableElement.prototype.getHorizontalAnchorEdge = function () {
5636         if ( this.computePosition && this.positioning && this.computePosition().right !== '' ) {
5637                 return 'right';
5638         }
5639         return 'left';
5643  * Return the side of the clippable on which it is "anchored" (aligned to something else).
5644  * ClippableElement will clip the opposite side when reducing element's width.
5646  * Classes that mix in ClippableElement should override this to return 'bottom' if their
5647  * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
5648  * If your class also mixes in FloatableElement, this is handled automatically.
5650  * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5651  * always in pixels, even if they were unset or set to 'auto'.)
5653  * When in doubt, 'top' is a reasonable fallback.
5655  * @return {string} 'top' or 'bottom'
5656  */
5657 OO.ui.mixin.ClippableElement.prototype.getVerticalAnchorEdge = function () {
5658         if ( this.computePosition && this.positioning && this.computePosition().bottom !== '' ) {
5659                 return 'bottom';
5660         }
5661         return 'top';
5665  * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5666  * when the element's natural height changes.
5668  * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5669  * overlapped by, the visible area of the nearest scrollable container.
5671  * Because calling clip() when the natural height changes isn't always possible, we also set
5672  * max-height when the element isn't being clipped. This means that if the element tries to grow
5673  * beyond the edge, something reasonable will happen before clip() is called.
5675  * @chainable
5676  * @return {OO.ui.Element} The element, for chaining
5677  */
5678 OO.ui.mixin.ClippableElement.prototype.clip = function () {
5679         if ( !this.clipping ) {
5680                 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below
5681                 // will fail
5682                 return this;
5683         }
5685         function rectCopy( rect ) {
5686                 return {
5687                         left: rect.left,
5688                         top: rect.top,
5689                         right: rect.right,
5690                         bottom: rect.bottom,
5691                         x: rect.x,
5692                         y: rect.y,
5693                         width: rect.width,
5694                         height: rect.height
5695                 };
5696         }
5698         function rectIntersection( a, b ) {
5699                 const out = {};
5700                 out.top = Math.max( a.top, b.top );
5701                 out.left = Math.max( a.left, b.left );
5702                 out.bottom = Math.min( a.bottom, b.bottom );
5703                 out.right = Math.min( a.right, b.right );
5704                 return out;
5705         }
5707         const viewportSpacing = OO.ui.getViewportSpacing();
5709         let $viewport, viewportRect;
5710         if ( this.$clippableScrollableContainer.is( 'html, body' ) ) {
5711                 $viewport = $( this.$clippableScrollableContainer[ 0 ].ownerDocument.body );
5712                 // Dimensions of the browser window, rather than the element!
5713                 viewportRect = {
5714                         top: 0,
5715                         left: 0,
5716                         right: document.documentElement.clientWidth,
5717                         bottom: document.documentElement.clientHeight
5718                 };
5719                 viewportRect.top += viewportSpacing.top;
5720                 viewportRect.left += viewportSpacing.left;
5721                 viewportRect.right -= viewportSpacing.right;
5722                 viewportRect.bottom -= viewportSpacing.bottom;
5723         } else {
5724                 $viewport = this.$clippableScrollableContainer;
5725                 viewportRect = $viewport[ 0 ].getBoundingClientRect();
5726                 // Convert into a plain object
5727                 viewportRect = rectCopy( viewportRect );
5728         }
5730         // Account for scrollbar gutter
5731         const direction = $viewport.css( 'direction' );
5732         const vertScrollbarWidth = $viewport.innerWidth() - $viewport.prop( 'clientWidth' );
5733         const horizScrollbarHeight = $viewport.innerHeight() - $viewport.prop( 'clientHeight' );
5734         viewportRect.bottom -= horizScrollbarHeight;
5735         if ( direction === 'rtl' ) {
5736                 viewportRect.left += vertScrollbarWidth;
5737         } else {
5738                 viewportRect.right -= vertScrollbarWidth;
5739         }
5741         // Extra tolerance so that the sloppy code below doesn't result in results that are off
5742         // by one or two pixels. (And also so that we have space to display drop shadows.)
5743         // Chosen by fair dice roll.
5744         const buffer = 7;
5745         viewportRect.top += buffer;
5746         viewportRect.left += buffer;
5747         viewportRect.right -= buffer;
5748         viewportRect.bottom -= buffer;
5750         const $item = this.$clippableContainer || this.$clippable;
5752         const extraHeight = $item.outerHeight() - this.$clippable.outerHeight();
5753         const extraWidth = $item.outerWidth() - this.$clippable.outerWidth();
5755         let itemRect = $item[ 0 ].getBoundingClientRect();
5756         // Convert into a plain object
5757         itemRect = rectCopy( itemRect );
5759         // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5760         // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5761         if ( this.getHorizontalAnchorEdge() === 'right' ) {
5762                 itemRect.left = viewportRect.left;
5763         } else {
5764                 itemRect.right = viewportRect.right;
5765         }
5766         if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5767                 itemRect.top = viewportRect.top;
5768         } else {
5769                 itemRect.bottom = viewportRect.bottom;
5770         }
5772         const availableRect = rectIntersection( viewportRect, itemRect );
5774         let desiredWidth = Math.max( 0, availableRect.right - availableRect.left );
5775         let desiredHeight = Math.max( 0, availableRect.bottom - availableRect.top );
5776         // It should never be desirable to exceed the dimensions of the browser viewport... right?
5777         desiredWidth = Math.min( desiredWidth,
5778                 document.documentElement.clientWidth - viewportSpacing.left - viewportSpacing.right );
5779         desiredHeight = Math.min( desiredHeight,
5780                 document.documentElement.clientHeight - viewportSpacing.top - viewportSpacing.right );
5781         const allotedWidth = Math.ceil( desiredWidth - extraWidth );
5782         const allotedHeight = Math.ceil( desiredHeight - extraHeight );
5783         const naturalWidth = this.$clippable.prop( 'scrollWidth' );
5784         const naturalHeight = this.$clippable.prop( 'scrollHeight' );
5785         const clipWidth = allotedWidth < naturalWidth;
5786         const clipHeight = allotedHeight < naturalHeight;
5788         if ( clipWidth ) {
5789                 // The hacks below are no longer needed for Firefox and Chrome after T349034,
5790                 // but may still be needed for Safari. TODO: Test and maybe remove them.
5792                 // Set overflow to 'scroll' first to avoid browser bugs causing bogus scrollbars (T67059),
5793                 // then to 'auto' which is what we want.
5794                 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5795                 // this case.
5796                 this.$clippable.css( 'overflowX', 'scroll' );
5797                 // eslint-disable-next-line no-unused-expressions
5798                 this.$clippable[ 0 ].offsetHeight; // Force reflow
5799                 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5800                 // See T157672.
5801                 this.$clippable.css( 'overflowX', 'auto' );
5802                 // eslint-disable-next-line no-unused-expressions
5803                 this.$clippable[ 0 ].offsetHeight; // Force reflow
5804                 this.$clippable.css( {
5805                         width: Math.max( 0, allotedWidth ),
5806                         maxWidth: ''
5807                 } );
5808         } else {
5809                 this.$clippable.css( {
5810                         overflowX: '',
5811                         width: this.idealWidth || '',
5812                         maxWidth: Math.max( 0, allotedWidth )
5813                 } );
5814         }
5815         if ( clipHeight ) {
5816                 // The hacks below are no longer needed for Firefox and Chrome after T349034,
5817                 // but may still be needed for Safari. TODO: Test and maybe remove them.
5819                 // Set overflow to 'scroll' first to avoid browser bugs causing bogus scrollbars (T67059),
5820                 // then to 'auto' which is what we want.
5821                 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5822                 // this case.
5823                 this.$clippable.css( 'overflowY', 'scroll' );
5824                 // eslint-disable-next-line no-unused-expressions
5825                 this.$clippable[ 0 ].offsetHeight; // Force reflow
5826                 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5827                 // See T157672.
5828                 this.$clippable.css( 'overflowY', 'auto' );
5829                 // eslint-disable-next-line no-unused-expressions
5830                 this.$clippable[ 0 ].offsetHeight; // Force reflow
5831                 this.$clippable.css( {
5832                         height: Math.max( 0, allotedHeight ),
5833                         maxHeight: ''
5834                 } );
5835         } else {
5836                 this.$clippable.css( {
5837                         overflowY: '',
5838                         height: this.idealHeight || '',
5839                         maxHeight: Math.max( 0, allotedHeight )
5840                 } );
5841         }
5843         // If we stopped clipping in at least one of the dimensions
5844         if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
5845                 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5846         }
5848         this.clippedHorizontally = clipWidth;
5849         this.clippedVertically = clipHeight;
5851         return this;
5855  * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5856  * By default, each popup has an anchor that points toward its origin.
5857  * Please see the [OOUI documentation on MediaWiki.org][1] for more information and examples.
5859  * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5861  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5863  *     @example
5864  *     // A PopupWidget.
5865  *     const popup = new OO.ui.PopupWidget( {
5866  *         $content: $( '<p>Hi there!</p>' ),
5867  *         padded: true,
5868  *         width: 300
5869  *     } );
5871  *     $( document.body ).append( popup.$element );
5872  *     // To display the popup, toggle the visibility to 'true'.
5873  *     popup.toggle( true );
5875  * @class
5876  * @extends OO.ui.Widget
5877  * @mixes OO.ui.mixin.IconElement
5878  * @mixes OO.ui.mixin.LabelElement
5879  * @mixes OO.ui.mixin.ClippableElement
5880  * @mixes OO.ui.mixin.FloatableElement
5882  * @constructor
5883  * @param {Object} [config] Configuration options
5884  * @param {number|null} [config.width=320] Width of popup in pixels. Pass `null` to use automatic width.
5885  * @param {number|null} [config.height=null] Height of popup in pixels. Pass `null` to use automatic height.
5886  * @param {boolean} [config.anchor=true] Show anchor pointing to origin of popup
5887  * @param {string} [config.position='below'] Where to position the popup relative to $floatableContainer
5888  *  'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5889  *    of $floatableContainer
5890  *  'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5891  *    of $floatableContainer
5892  *  'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5893  *    endwards (right/left) to the vertical center of $floatableContainer
5894  *  'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5895  *    startwards (left/right) to the vertical center of $floatableContainer
5896  * @param {string} [config.align='center'] How to align the popup to $floatableContainer
5897  *  'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in
5898  *    RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5899  *    move the popup as far downwards as possible.
5900  *  'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in
5901  *    RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5902  *     move the popup as far upwards as possible.
5903  *  'center': Horizontally (if position is above/below) or vertically (before/after) align the
5904  *     center of the popup with the center of $floatableContainer.
5905  * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5906  * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5907  * @param {boolean} [config.autoFlip=true] Whether to automatically switch the popup's position between
5908  *  'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5909  *  desired direction to display the popup without clipping
5910  * @param {jQuery} [config.$container] Constrain the popup to the boundaries of the specified container.
5911  *  See the [OOUI docs on MediaWiki][3] for an example.
5912  *  [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5913  * @param {number} [config.containerPadding=10] Padding between the popup and its container, specified as a
5914  *  number of pixels.
5915  * @param {jQuery} [config.$content] Content to append to the popup's body
5916  * @param {jQuery} [config.$footer] Content to append to the popup's footer
5917  * @param {boolean} [config.autoClose=false] Automatically close the popup when it loses focus.
5918  * @param {jQuery} [config.$autoCloseIgnore] Elements that will not close the popup when clicked.
5919  *  This config option is only relevant if #autoClose is set to `true`. See the
5920  *  [OOUI documentation on MediaWiki][2] for an example.
5921  *  [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5922  * @param {boolean} [config.head=false] Show a popup header that contains a #label (if specified) and close
5923  *  button.
5924  * @param {boolean} [config.hideCloseButton=false]
5925  * @param {boolean} [config.padded=false] Add padding to the popup's body
5926  */
5927 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
5928         // Configuration initialization
5929         config = config || {};
5931         // Parent constructor
5932         OO.ui.PopupWidget.super.call( this, config );
5934         // Properties (must be set before ClippableElement constructor call)
5935         this.$body = $( '<div>' );
5936         this.$popup = $( '<div>' );
5938         // Mixin constructors
5939         OO.ui.mixin.IconElement.call( this, config );
5940         OO.ui.mixin.LabelElement.call( this, config );
5941         OO.ui.mixin.ClippableElement.call( this, Object.assign( {
5942                 $clippable: this.$body,
5943                 $clippableContainer: this.$popup
5944         }, config ) );
5945         OO.ui.mixin.FloatableElement.call( this, config );
5947         // Properties
5948         this.$anchor = $( '<div>' );
5949         // If undefined, will be computed lazily in computePosition()
5950         this.$container = config.$container;
5951         this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
5952         this.autoClose = !!config.autoClose;
5953         this.transitionTimeout = null;
5954         this.anchored = false;
5955         this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
5956         this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
5957         this.onTabKeyDownHandler = this.onTabKeyDown.bind( this );
5958         this.onShiftTabKeyDownHandler = this.onShiftTabKeyDown.bind( this );
5960         // Initialization
5961         this.setSize( config.width, config.height );
5962         this.toggleAnchor( config.anchor === undefined || config.anchor );
5963         this.setAlignment( config.align || 'center' );
5964         this.setPosition( config.position || 'below' );
5965         this.setAutoFlip( config.autoFlip === undefined || config.autoFlip );
5966         this.setAutoCloseIgnore( config.$autoCloseIgnore );
5967         this.$body.addClass( 'oo-ui-popupWidget-body' );
5968         this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
5969         this.$popup
5970                 .addClass( 'oo-ui-popupWidget-popup' )
5971                 .append( this.$body );
5972         this.$element
5973                 .addClass( 'oo-ui-popupWidget' )
5974                 .append( this.$popup, this.$anchor );
5975         // Move content, which was added to #$element by OO.ui.Widget, to the body
5976         // FIXME This is gross, we should use '$body' or something for the config
5977         if ( config.$content instanceof $ ) {
5978                 this.$body.append( config.$content );
5979         }
5981         this.padded = !!config.padded;
5982         if ( config.padded ) {
5983                 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
5984         }
5986         if ( config.head ) {
5987                 if ( !config.hideCloseButton ) {
5988                         this.closeButton = new OO.ui.ButtonWidget( {
5989                                 framed: false,
5990                                 icon: 'close',
5991                                 label: OO.ui.msg( 'ooui-popup-widget-close-button-aria-label' ),
5992                                 invisibleLabel: true
5993                         } );
5994                         this.closeButton.connect( this, {
5995                                 click: 'onCloseButtonClick'
5996                         } );
5997                 }
5998                 this.$head = $( '<div>' )
5999                         .addClass( 'oo-ui-popupWidget-head' )
6000                         .append( this.$icon, this.$label, this.closeButton && this.closeButton.$element );
6001                 this.$popup.prepend( this.$head );
6002         }
6004         if ( config.$footer ) {
6005                 this.$footer = $( '<div>' )
6006                         .addClass( 'oo-ui-popupWidget-footer' )
6007                         .append( config.$footer );
6008                 this.$popup.append( this.$footer );
6009         }
6011         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
6012         // that reference properties not initialized at that time of parent class construction
6013         // TODO: Find a better way to handle post-constructor setup
6014         this.visible = false;
6015         this.$element.addClass( 'oo-ui-element-hidden' );
6018 /* Setup */
6020 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
6021 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.IconElement );
6022 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
6023 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
6024 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.FloatableElement );
6026 /* Events */
6029  * The popup is ready: it is visible and has been positioned and clipped.
6031  * @event OO.ui.PopupWidget#ready
6032  */
6035  * The popup is no longer visible.
6037  * @event OO.ui.PopupWidget#closing
6038  */
6040 /* Methods */
6043  * Handles document mouse down events.
6045  * @private
6046  * @param {MouseEvent} e Mouse down event
6047  */
6048 OO.ui.PopupWidget.prototype.onDocumentMouseDown = function ( e ) {
6049         if (
6050                 this.isVisible() &&
6051                 !OO.ui.contains( this.$element.add( this.$autoCloseIgnore ).get(), e.target, true )
6052         ) {
6053                 this.toggle( false );
6054         }
6058  * Bind document mouse down listener.
6060  * @private
6061  */
6062 OO.ui.PopupWidget.prototype.bindDocumentMouseDownListener = function () {
6063         // Capture clicks outside popup
6064         this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
6065         // We add 'click' event because iOS safari needs to respond to this event.
6066         // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
6067         // then it will trigger when scrolling. While iOS Safari has some reported behavior
6068         // of occasionally not emitting 'click' properly, that event seems to be the standard
6069         // that it should be emitting, so we add it to this and will operate the event handler
6070         // on whichever of these events was triggered first
6071         this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler, true );
6075  * Handles close button click events.
6077  * @private
6078  */
6079 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
6080         if ( this.isVisible() ) {
6081                 this.toggle( false );
6082         }
6086  * Unbind document mouse down listener.
6088  * @private
6089  */
6090 OO.ui.PopupWidget.prototype.unbindDocumentMouseDownListener = function () {
6091         this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
6092         this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler, true );
6096  * Handles document key down events.
6098  * @private
6099  * @param {KeyboardEvent} e Key down event
6100  */
6101 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
6102         if (
6103                 e.which === OO.ui.Keys.ESCAPE &&
6104                 this.isVisible()
6105         ) {
6106                 this.toggle( false );
6107                 e.preventDefault();
6108                 e.stopPropagation();
6109         }
6113  * Bind document key down listener.
6115  * @private
6116  */
6117 OO.ui.PopupWidget.prototype.bindDocumentKeyDownListener = function () {
6118         this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
6122  * Unbind document key down listener.
6124  * @private
6125  */
6126 OO.ui.PopupWidget.prototype.unbindDocumentKeyDownListener = function () {
6127         this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
6131  * Handles Tab key down events.
6133  * @private
6134  * @param {KeyboardEvent} e Key down event
6135  */
6136 OO.ui.PopupWidget.prototype.onTabKeyDown = function ( e ) {
6137         if ( !e.shiftKey && e.which === OO.ui.Keys.TAB ) {
6138                 e.preventDefault();
6139                 this.toggle( false );
6140         }
6144  * Handles Shift + Tab key down events.
6146  * @private
6147  * @param {KeyboardEvent} e Key down event
6148  */
6149 OO.ui.PopupWidget.prototype.onShiftTabKeyDown = function ( e ) {
6150         if ( e.shiftKey && e.which === OO.ui.Keys.TAB ) {
6151                 e.preventDefault();
6152                 this.toggle( false );
6153         }
6157  * Show, hide, or toggle the visibility of the anchor.
6159  * @param {boolean} [show] Show anchor, omit to toggle
6160  */
6161 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
6162         show = show === undefined ? !this.anchored : !!show;
6164         if ( this.anchored !== show ) {
6165                 this.$element.toggleClass( 'oo-ui-popupWidget-anchored oo-ui-popupWidget-anchored-' + this.anchorEdge, show );
6166                 this.anchored = show;
6167         }
6171  * Change which edge the anchor appears on.
6173  * @param {string} edge 'top', 'bottom', 'start' or 'end'
6174  */
6175 OO.ui.PopupWidget.prototype.setAnchorEdge = function ( edge ) {
6176         if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge ) === -1 ) {
6177                 throw new Error( 'Invalid value for edge: ' + edge );
6178         }
6179         if ( this.anchorEdge !== null ) {
6180                 this.$element.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge );
6181         }
6182         this.anchorEdge = edge;
6183         if ( this.anchored ) {
6184                 this.$element.addClass( 'oo-ui-popupWidget-anchored-' + edge );
6185         }
6189  * Check if the anchor is visible.
6191  * @return {boolean} Anchor is visible
6192  */
6193 OO.ui.PopupWidget.prototype.hasAnchor = function () {
6194         return this.anchored;
6198  * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
6199  * `.toggle( true )` after its #$element is attached to the DOM.
6201  * Do not show the popup while it is not attached to the DOM. The calculations required to display
6202  * it in the right place and with the right dimensions only work correctly while it is attached.
6203  * Side-effects may include broken interface and exceptions being thrown. This wasn't always
6204  * strictly enforced, so currently it only generates a warning in the browser console.
6206  * @fires OO.ui.PopupWidget#ready
6207  * @inheritdoc
6208  */
6209 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
6210         show = show === undefined ? !this.isVisible() : !!show;
6212         const change = show !== this.isVisible();
6214         if ( show && !this.warnedUnattached && !this.isElementAttached() ) {
6215                 OO.ui.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
6216                 this.warnedUnattached = true;
6217         }
6218         if ( show && !this.$floatableContainer && this.isElementAttached() ) {
6219                 // Fall back to the parent node if the floatableContainer is not set
6220                 this.setFloatableContainer( this.$element.parent() );
6221         }
6223         if ( change && show && this.autoFlip ) {
6224                 // Reset auto-flipping before showing the popup again. It's possible we no longer need to
6225                 // flip (e.g. if the user scrolled).
6226                 this.isAutoFlipped = false;
6227         }
6229         // Parent method
6230         OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
6232         if ( change ) {
6233                 this.togglePositioning( show && !!this.$floatableContainer );
6235                 // Find the first and last focusable element in the popup widget
6236                 const $lastFocusableElement = OO.ui.findFocusable( this.$element, true );
6237                 const $firstFocusableElement = OO.ui.findFocusable( this.$element, false );
6239                 if ( show ) {
6240                         if ( this.autoClose ) {
6241                                 this.bindDocumentMouseDownListener();
6242                                 this.bindDocumentKeyDownListener();
6244                                 // Bind a keydown event to the first and last focusable element
6245                                 // If user presses the tab key on this item, dismiss the popup and
6246                                 // take focus back to the caller, ideally the caller implements this functionality
6247                                 // This is to prevent illogical focus order, a common accessibility pitfall.
6248                                 // Alternative Fix: Implement focus trap for popup widget.
6249                                 $lastFocusableElement.on( 'keydown', this.onTabKeyDownHandler );
6250                                 $firstFocusableElement.on( 'keydown', this.onShiftTabKeyDownHandler );
6251                         }
6252                         this.updateDimensions();
6253                         this.toggleClipping( true );
6255                         if ( this.autoFlip ) {
6256                                 if ( this.popupPosition === 'above' || this.popupPosition === 'below' ) {
6257                                         if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
6258                                                 // If opening the popup in the normal direction causes it to be clipped,
6259                                                 // open in the opposite one instead
6260                                                 const normalHeight = this.$element.height();
6261                                                 this.isAutoFlipped = !this.isAutoFlipped;
6262                                                 this.position();
6263                                                 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
6264                                                         // If that also causes it to be clipped, open in whichever direction
6265                                                         // we have more space
6266                                                         const oppositeHeight = this.$element.height();
6267                                                         if ( oppositeHeight < normalHeight ) {
6268                                                                 this.isAutoFlipped = !this.isAutoFlipped;
6269                                                                 this.position();
6270                                                         }
6271                                                 }
6272                                         }
6273                                 }
6274                                 if ( this.popupPosition === 'before' || this.popupPosition === 'after' ) {
6275                                         if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
6276                                                 // If opening the popup in the normal direction causes it to be clipped,
6277                                                 // open in the opposite one instead
6278                                                 const normalWidth = this.$element.width();
6279                                                 this.isAutoFlipped = !this.isAutoFlipped;
6280                                                 // Due to T180173 horizontally clipped PopupWidgets have messed up
6281                                                 // dimensions, which causes positioning to be off. Toggle clipping back and
6282                                                 // forth to work around.
6283                                                 this.toggleClipping( false );
6284                                                 this.position();
6285                                                 this.toggleClipping( true );
6286                                                 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
6287                                                         // If that also causes it to be clipped, open in whichever direction
6288                                                         // we have more space
6289                                                         const oppositeWidth = this.$element.width();
6290                                                         if ( oppositeWidth < normalWidth ) {
6291                                                                 this.isAutoFlipped = !this.isAutoFlipped;
6292                                                                 // Due to T180173, horizontally clipped PopupWidgets have messed up
6293                                                                 // dimensions, which causes positioning to be off. Toggle clipping
6294                                                                 // back and forth to work around.
6295                                                                 this.toggleClipping( false );
6296                                                                 this.position();
6297                                                                 this.toggleClipping( true );
6298                                                         }
6299                                                 }
6300                                         }
6301                                 }
6302                         }
6304                         this.emit( 'ready' );
6305                 } else {
6306                         this.toggleClipping( false );
6307                         if ( this.autoClose ) {
6308                                 // Remove binded keydown event from the first and last focusable elements
6309                                 // when popup closes
6310                                 $lastFocusableElement.off( 'keydown', this.onTabKeyDownHandler );
6311                                 $firstFocusableElement.off( 'keydown', this.onShiftTabKeyDownHandler );
6313                                 this.unbindDocumentMouseDownListener();
6314                                 this.unbindDocumentKeyDownListener();
6315                         }
6317                         // This is so we can restore focus to the parent when the pop widget dismisses
6318                         // Also, we're emitting an event
6319                         // so we don't have to tie this implementation to the caller.
6320                         // Let the caller handle this details.
6321                         this.emit( 'closing' );
6322                 }
6323         }
6325         return this;
6329  * Set the size of the popup.
6331  * Changing the size may also change the popup's position depending on the alignment.
6333  * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
6334  * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
6335  * @param {boolean} [transition=false] Use a smooth transition
6336  * @chainable
6337  */
6338 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
6339         this.width = width !== undefined ? width : 320;
6340         this.height = height !== undefined ? height : null;
6341         if ( this.isVisible() ) {
6342                 this.updateDimensions( transition );
6343         }
6347  * Update the size and position.
6349  * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
6350  * be called automatically.
6352  * @param {boolean} [transition=false] Use a smooth transition
6353  * @chainable
6354  */
6355 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
6356         // Prevent transition from being interrupted
6357         clearTimeout( this.transitionTimeout );
6358         if ( transition ) {
6359                 // Enable transition
6360                 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
6361         }
6363         this.position();
6365         if ( transition ) {
6366                 // Prevent transitioning after transition is complete
6367                 this.transitionTimeout = setTimeout( () => {
6368                         this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
6369                 }, 200 );
6370         } else {
6371                 // Prevent transitioning immediately
6372                 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
6373         }
6377  * @inheritdoc
6378  */
6379 OO.ui.PopupWidget.prototype.computePosition = function () {
6380         const popupPos = {},
6381                 anchorCss = { left: '', right: '', top: '', bottom: '' },
6382                 popupPositionOppositeMap = {
6383                         above: 'below',
6384                         below: 'above',
6385                         before: 'after',
6386                         after: 'before'
6387                 },
6388                 alignMap = {
6389                         ltr: {
6390                                 'force-left': 'backwards',
6391                                 'force-right': 'forwards'
6392                         },
6393                         rtl: {
6394                                 'force-left': 'forwards',
6395                                 'force-right': 'backwards'
6396                         }
6397                 },
6398                 anchorEdgeMap = {
6399                         above: 'bottom',
6400                         below: 'top',
6401                         before: 'end',
6402                         after: 'start'
6403                 },
6404                 hPosMap = {
6405                         forwards: 'start',
6406                         center: 'center',
6407                         backwards: this.anchored ? 'before' : 'end'
6408                 },
6409                 vPosMap = {
6410                         forwards: 'top',
6411                         center: 'center',
6412                         backwards: 'bottom'
6413                 };
6415         if ( !this.$container ) {
6416                 // Lazy-initialize $container if not specified in constructor
6417                 this.$container = $( this.getClosestScrollableElementContainer() );
6418         }
6419         const direction = this.$container.css( 'direction' );
6421         // Set height and width before we do anything else, since it might cause our measurements
6422         // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
6423         this.setIdealSize(
6424                 // The properties refer to the width of this.$popup, but we set the properties on this.$body
6425                 // to make calculations work out right (T180173), so we subtract padding here.
6426                 this.width !== null ? this.width - ( this.padded ? 24 : 0 ) : 'auto',
6427                 this.height !== null ? this.height - ( this.padded ? 10 : 0 ) : 'auto'
6428         );
6430         const align = alignMap[ direction ][ this.align ] || this.align;
6431         let popupPosition = this.popupPosition;
6432         if ( this.isAutoFlipped ) {
6433                 popupPosition = popupPositionOppositeMap[ popupPosition ];
6434         }
6436         // If the popup is positioned before or after, then the anchor positioning is vertical,
6437         // otherwise horizontal
6438         const vertical = popupPosition === 'before' || popupPosition === 'after';
6439         const start = vertical ? 'top' : ( direction === 'rtl' ? 'right' : 'left' );
6440         const end = vertical ? 'bottom' : ( direction === 'rtl' ? 'left' : 'right' );
6441         const near = vertical ? 'top' : 'left';
6442         const far = vertical ? 'bottom' : 'right';
6443         const sizeProp = vertical ? 'Height' : 'Width';
6444         const popupSize = vertical ? this.$popup.height() : this.$popup.width();
6446         this.setAnchorEdge( anchorEdgeMap[ popupPosition ] );
6447         this.horizontalPosition = vertical ? popupPosition : hPosMap[ align ];
6448         this.verticalPosition = vertical ? vPosMap[ align ] : popupPosition;
6450         // Parent method
6451         const parentPosition = OO.ui.mixin.FloatableElement.prototype.computePosition.call( this );
6452         // Find out which property FloatableElement used for positioning, and adjust that value
6453         const positionProp = vertical ?
6454                 ( parentPosition.top !== '' ? 'top' : 'bottom' ) :
6455                 ( parentPosition.left !== '' ? 'left' : 'right' );
6457         // Figure out where the near and far edges of the popup and $floatableContainer are
6458         const floatablePos = this.$floatableContainer.offset();
6459         floatablePos[ far ] = floatablePos[ near ] + this.$floatableContainer[ 'outer' + sizeProp ]();
6460         // Measure where the offsetParent is and compute our position based on that and parentPosition
6461         const offsetParentPos = this.$element.offsetParent()[ 0 ] === document.documentElement ?
6462                 { top: 0, left: 0 } :
6463                 this.$element.offsetParent().offset();
6465         if ( positionProp === near ) {
6466                 popupPos[ near ] = offsetParentPos[ near ] + parentPosition[ near ];
6467                 popupPos[ far ] = popupPos[ near ] + popupSize;
6468         } else {
6469                 popupPos[ far ] = offsetParentPos[ near ] +
6470                         this.$element.offsetParent()[ 'inner' + sizeProp ]() - parentPosition[ far ];
6471                 popupPos[ near ] = popupPos[ far ] - popupSize;
6472         }
6474         let anchorOffset, positionAdjustment;
6475         if ( this.anchored ) {
6476                 // Position the anchor (which is positioned relative to the popup) to point to
6477                 // $floatableContainer
6478                 const anchorPos = ( floatablePos[ start ] + floatablePos[ end ] ) / 2;
6479                 anchorOffset = ( start === far ? -1 : 1 ) * ( anchorPos - popupPos[ start ] );
6481                 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more
6482                 // space this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use
6483                 // scrollWidth/Height
6484                 const anchorSize = this.$anchor[ 0 ][ 'scroll' + sizeProp ];
6485                 const anchorMargin = parseFloat( this.$anchor.css( 'margin-' + start ) );
6486                 if ( anchorOffset + anchorMargin < 2 * anchorSize ) {
6487                         // Not enough space for the anchor on the start side; pull the popup startwards
6488                         positionAdjustment = ( positionProp === start ? -1 : 1 ) *
6489                                 ( 2 * anchorSize - ( anchorOffset + anchorMargin ) );
6490                 } else if ( anchorOffset + anchorMargin > popupSize - 2 * anchorSize ) {
6491                         // Not enough space for the anchor on the end side; pull the popup endwards
6492                         positionAdjustment = ( positionProp === end ? -1 : 1 ) *
6493                                 ( anchorOffset + anchorMargin - ( popupSize - 2 * anchorSize ) );
6494                 } else {
6495                         positionAdjustment = 0;
6496                 }
6497         } else {
6498                 positionAdjustment = 0;
6499         }
6501         // Check if the popup will go beyond the edge of this.$container
6502         const containerPos = this.$container[ 0 ] === document.documentElement ?
6503                 { top: 0, left: 0 } :
6504                 this.$container.offset();
6505         containerPos[ far ] = containerPos[ near ] + this.$container[ 'inner' + sizeProp ]();
6506         if ( this.$container[ 0 ] === document.documentElement ) {
6507                 const viewportSpacing = OO.ui.getViewportSpacing();
6508                 containerPos[ near ] += viewportSpacing[ near ];
6509                 containerPos[ far ] -= viewportSpacing[ far ];
6510         }
6511         // Take into account how much the popup will move because of the adjustments we're going to make
6512         popupPos[ near ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment;
6513         popupPos[ far ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment;
6514         if ( containerPos[ near ] + this.containerPadding > popupPos[ near ] ) {
6515                 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
6516                 positionAdjustment += ( positionProp === near ? 1 : -1 ) *
6517                         ( containerPos[ near ] + this.containerPadding - popupPos[ near ] );
6518         } else if ( containerPos[ far ] - this.containerPadding < popupPos[ far ] ) {
6519                 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
6520                 positionAdjustment += ( positionProp === far ? 1 : -1 ) *
6521                         ( popupPos[ far ] - ( containerPos[ far ] - this.containerPadding ) );
6522         }
6524         if ( this.anchored ) {
6525                 // Adjust anchorOffset for positionAdjustment
6526                 anchorOffset += ( positionProp === start ? -1 : 1 ) * positionAdjustment;
6528                 // Position the anchor
6529                 anchorCss[ start ] = anchorOffset;
6530                 this.$anchor.css( anchorCss );
6531         }
6533         // Move the popup if needed
6534         parentPosition[ positionProp ] += positionAdjustment;
6536         return parentPosition;
6540  * Set popup alignment
6542  * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
6543  *  `backwards` or `forwards`.
6544  */
6545 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
6546         // Validate alignment
6547         if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
6548                 this.align = align;
6549         } else {
6550                 this.align = 'center';
6551         }
6552         this.position();
6556  * Get popup alignment
6558  * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
6559  *  `backwards` or `forwards`.
6560  */
6561 OO.ui.PopupWidget.prototype.getAlignment = function () {
6562         return this.align;
6566  * Change the positioning of the popup.
6568  * @param {string} position 'above', 'below', 'before' or 'after'
6569  */
6570 OO.ui.PopupWidget.prototype.setPosition = function ( position ) {
6571         if ( [ 'above', 'below', 'before', 'after' ].indexOf( position ) === -1 ) {
6572                 position = 'below';
6573         }
6574         this.popupPosition = position;
6575         this.position();
6579  * Get popup positioning.
6581  * @return {string} 'above', 'below', 'before' or 'after'
6582  */
6583 OO.ui.PopupWidget.prototype.getPosition = function () {
6584         return this.popupPosition;
6588  * Set popup auto-flipping.
6590  * @param {boolean} [autoFlip=false] Whether to automatically switch the popup's position between
6591  *  'above' and 'below', or between 'before' and 'after', if there is not enough space in the
6592  *  desired direction to display the popup without clipping
6593  */
6594 OO.ui.PopupWidget.prototype.setAutoFlip = function ( autoFlip ) {
6595         autoFlip = !!autoFlip;
6597         if ( this.autoFlip !== autoFlip ) {
6598                 this.autoFlip = autoFlip;
6599         }
6603  * Set which elements will not close the popup when clicked.
6605  * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
6607  * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
6608  */
6609 OO.ui.PopupWidget.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore ) {
6610         this.$autoCloseIgnore = $autoCloseIgnore;
6614  * Get an ID of the body element, this can be used as the
6615  * `aria-describedby` attribute for an input field.
6617  * @return {string} The ID of the body element
6618  */
6619 OO.ui.PopupWidget.prototype.getBodyId = function () {
6620         let id = this.$body.attr( 'id' );
6621         if ( id === undefined ) {
6622                 id = OO.ui.generateElementId();
6623                 this.$body.attr( 'id', id );
6624         }
6625         return id;
6629  * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6630  * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6631  * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6632  * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6634  * @abstract
6635  * @class
6637  * @constructor
6638  * @param {Object} [config] Configuration options
6639  * @param {Object} [config.popup] Configuration to pass to popup
6640  * @param {boolean} [config.popup.autoClose=true] Popup auto-closes when it loses focus
6641  */
6642 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
6643         // Configuration initialization
6644         config = config || {};
6646         // Properties
6647         this.popup = new OO.ui.PopupWidget( Object.assign(
6648                 {
6649                         autoClose: true,
6650                         $floatableContainer: this.$element
6651                 },
6652                 config.popup,
6653                 {
6654                         $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore )
6655                 }
6656         ) );
6659 /* Methods */
6662  * Get popup.
6664  * @return {OO.ui.PopupWidget} Popup widget
6665  */
6666 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
6667         return this.popup;
6671  * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
6672  * which is used to display additional information or options.
6674  *     @example
6675  *     // A PopupButtonWidget.
6676  *     const popupButton = new OO.ui.PopupButtonWidget( {
6677  *         label: 'Popup button with options',
6678  *         icon: 'menu',
6679  *         popup: {
6680  *             $content: $( '<p>Additional options here.</p>' ),
6681  *             padded: true,
6682  *             align: 'force-left'
6683  *         }
6684  *     } );
6685  *     // Append the button to the DOM.
6686  *     $( document.body ).append( popupButton.$element );
6688  * @class
6689  * @extends OO.ui.ButtonWidget
6690  * @mixes OO.ui.mixin.PopupElement
6692  * @constructor
6693  * @param {Object} [config] Configuration options
6694  * @param {jQuery} [config.$overlay] Render the popup into a separate layer. This configuration is useful
6695  *  in cases where the expanded popup is larger than its containing `<div>`. The specified overlay
6696  *  layer is usually on top of the containing `<div>` and has a larger area. By default, the popup
6697  *  uses relative positioning.
6698  *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
6699  */
6700 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
6701         // Configuration initialization
6702         config = config || {};
6704         // Parent constructor
6705         OO.ui.PopupButtonWidget.super.call( this, config );
6707         // Mixin constructors
6708         OO.ui.mixin.PopupElement.call( this, config );
6710         // Properties
6711         this.$overlay = ( config.$overlay === true ?
6712                 OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
6714         // Events
6715         this.connect( this, {
6716                 click: 'onAction'
6717         } );
6719         // Initialization
6720         this.$element.addClass( 'oo-ui-popupButtonWidget' );
6721         this.$button.attr( {
6722                 'aria-haspopup': 'dialog',
6723                 'aria-owns': this.popup.getElementId()
6724         } );
6725         this.popup.$element
6726                 .addClass( 'oo-ui-popupButtonWidget-popup' )
6727                 .attr( {
6728                         role: 'dialog',
6729                         'aria-describedby': this.getElementId()
6730                 } )
6731                 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6732                 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6733         this.$overlay.append( this.popup.$element );
6736 /* Setup */
6738 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
6739 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
6741 /* Methods */
6744  * Handle the button action being triggered.
6746  * @private
6747  */
6748 OO.ui.PopupButtonWidget.prototype.onAction = function () {
6749         this.popup.toggle();
6753  * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6755  * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6757  * @private
6758  * @abstract
6759  * @class
6760  * @mixes OO.ui.mixin.GroupElement
6762  * @constructor
6763  * @param {Object} [config] Configuration options
6764  */
6765 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
6766         // Mixin constructors
6767         OO.ui.mixin.GroupElement.call( this, config );
6770 /* Setup */
6772 OO.mixinClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
6774 /* Methods */
6777  * Set the disabled state of the widget.
6779  * This will also update the disabled state of child widgets.
6781  * @param {boolean} [disabled=false] Disable widget
6782  * @chainable
6783  * @return {OO.ui.Widget} The widget, for chaining
6784  */
6785 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
6786         // Parent method
6787         // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6788         OO.ui.Widget.prototype.setDisabled.call( this, disabled );
6790         // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6791         if ( this.items ) {
6792                 for ( let i = 0, len = this.items.length; i < len; i++ ) {
6793                         this.items[ i ].updateDisabled();
6794                 }
6795         }
6797         return this;
6801  * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6803  * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group.
6804  * This allows bidirectional communication.
6806  * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6808  * @private
6809  * @abstract
6810  * @class
6812  * @constructor
6813  */
6814 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
6815         //
6818 /* Methods */
6821  * Check if widget is disabled.
6823  * Checks parent if present, making disabled state inheritable.
6825  * @return {boolean} Widget is disabled
6826  */
6827 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
6828         return this.disabled ||
6829                 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
6833  * Set group element is in.
6835  * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6836  * @chainable
6837  * @return {OO.ui.Widget} The widget, for chaining
6838  */
6839 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
6840         // Parent method
6841         // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6842         OO.ui.Element.prototype.setElementGroup.call( this, group );
6844         // Initialize item disabled states
6845         this.updateDisabled();
6847         return this;
6851  * OptionWidgets are special elements that can be selected and configured with data. The
6852  * data is often unique for each option, but it does not have to be. OptionWidgets are used
6853  * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6854  * and examples, please see the [OOUI documentation on MediaWiki][1].
6856  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6858  * @class
6859  * @extends OO.ui.Widget
6860  * @mixes OO.ui.mixin.ItemWidget
6861  * @mixes OO.ui.mixin.LabelElement
6862  * @mixes OO.ui.mixin.FlaggedElement
6863  * @mixes OO.ui.mixin.AccessKeyedElement
6864  * @mixes OO.ui.mixin.TitledElement
6866  * @constructor
6867  * @param {Object} [config] Configuration options
6868  * @param {boolean} [config.selected=false]
6869  */
6870 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
6871         // Configuration initialization
6872         config = config || {};
6874         // Parent constructor
6875         OO.ui.OptionWidget.super.call( this, config );
6877         // Mixin constructors
6878         OO.ui.mixin.ItemWidget.call( this );
6879         OO.ui.mixin.LabelElement.call( this, config );
6880         OO.ui.mixin.FlaggedElement.call( this, config );
6881         OO.ui.mixin.AccessKeyedElement.call( this, config );
6882         OO.ui.mixin.TitledElement.call( this, config );
6884         // Properties
6885         this.highlighted = false;
6886         this.pressed = false;
6887         this.setSelected( !!config.selected );
6889         // Initialization
6890         this.$element
6891                 .data( 'oo-ui-optionWidget', this )
6892                 // Allow programmatic focussing (and by access key), but not tabbing
6893                 .attr( {
6894                         tabindex: '-1',
6895                         role: 'option'
6896                 } )
6897                 .addClass( 'oo-ui-optionWidget' )
6898                 .append( this.$label );
6901 /* Setup */
6903 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
6904 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
6905 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
6906 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
6907 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.AccessKeyedElement );
6908 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.TitledElement );
6910 /* Static Properties */
6913  * Whether this option can be selected. See #setSelected.
6915  * @static
6916  * @property {boolean}
6917  */
6918 OO.ui.OptionWidget.static.selectable = true;
6921  * Whether this option can be highlighted. See #setHighlighted.
6923  * @static
6924  * @property {boolean}
6925  */
6926 OO.ui.OptionWidget.static.highlightable = true;
6929  * Whether this option can be pressed. See #setPressed.
6931  * @static
6932  * @property {boolean}
6933  */
6934 OO.ui.OptionWidget.static.pressable = true;
6937  * Whether this option will be scrolled into view when it is selected.
6939  * @static
6940  * @property {boolean}
6941  */
6942 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
6944 /* Methods */
6947  * Check if the option can be selected.
6949  * @return {boolean} Item is selectable
6950  */
6951 OO.ui.OptionWidget.prototype.isSelectable = function () {
6952         return this.constructor.static.selectable && !this.disabled && this.isVisible();
6956  * Check if the option can be highlighted. A highlight indicates that the option
6957  * may be selected when a user presses Enter key or clicks. Disabled items cannot
6958  * be highlighted.
6960  * @return {boolean} Item is highlightable
6961  */
6962 OO.ui.OptionWidget.prototype.isHighlightable = function () {
6963         return this.constructor.static.highlightable && !this.disabled && this.isVisible();
6967  * Check if the option can be pressed. The pressed state occurs when a user mouses
6968  * down on an item, but has not yet let go of the mouse.
6970  * @return {boolean} Item is pressable
6971  */
6972 OO.ui.OptionWidget.prototype.isPressable = function () {
6973         return this.constructor.static.pressable && !this.disabled && this.isVisible();
6977  * Check if the option is selected.
6979  * @return {boolean} Item is selected
6980  */
6981 OO.ui.OptionWidget.prototype.isSelected = function () {
6982         return this.selected;
6986  * Check if the option is highlighted. A highlight indicates that the
6987  * item may be selected when a user presses Enter key or clicks.
6989  * @return {boolean} Item is highlighted
6990  */
6991 OO.ui.OptionWidget.prototype.isHighlighted = function () {
6992         return this.highlighted;
6996  * Check if the option is pressed. The pressed state occurs when a user mouses
6997  * down on an item, but has not yet let go of the mouse. The item may appear
6998  * selected, but it will not be selected until the user releases the mouse.
7000  * @return {boolean} Item is pressed
7001  */
7002 OO.ui.OptionWidget.prototype.isPressed = function () {
7003         return this.pressed;
7007  * Set the option’s selected state. In general, all modifications to the selection
7008  * should be handled by the SelectWidget’s
7009  * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
7011  * @param {boolean} [state=false] Select option
7012  * @chainable
7013  * @return {OO.ui.Widget} The widget, for chaining
7014  */
7015 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
7016         if ( this.constructor.static.selectable ) {
7017                 this.selected = !!state;
7018                 this.$element
7019                         .toggleClass( 'oo-ui-optionWidget-selected', state )
7020                         .attr( 'aria-selected', this.selected.toString() );
7021                 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
7022                         this.scrollElementIntoView();
7023                 }
7024                 this.updateThemeClasses();
7025         }
7026         return this;
7030  * Set the option’s highlighted state. In general, all programmatic
7031  * modifications to the highlight should be handled by the
7032  * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
7033  * method instead of this method.
7035  * @param {boolean} [state=false] Highlight option
7036  * @chainable
7037  * @return {OO.ui.Widget} The widget, for chaining
7038  */
7039 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
7040         if ( this.constructor.static.highlightable ) {
7041                 this.highlighted = !!state;
7042                 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
7043                 this.updateThemeClasses();
7044         }
7045         return this;
7049  * Set the option’s pressed state. In general, all
7050  * programmatic modifications to the pressed state should be handled by the
7051  * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
7052  * method instead of this method.
7054  * @param {boolean} [state=false] Press option
7055  * @chainable
7056  * @return {OO.ui.Widget} The widget, for chaining
7057  */
7058 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
7059         if ( this.constructor.static.pressable ) {
7060                 this.pressed = !!state;
7061                 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
7062                 this.updateThemeClasses();
7063         }
7064         return this;
7068  * Get text to match search strings against.
7070  * The default implementation returns the label text, but subclasses
7071  * can override this to provide more complex behavior.
7073  * @return {string|boolean} String to match search string against
7074  */
7075 OO.ui.OptionWidget.prototype.getMatchText = function () {
7076         const label = this.getLabel();
7077         return typeof label === 'string' ? label : this.$label.text();
7081  * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
7082  * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
7083  * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
7084  * menu selects}.
7086  * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For
7087  * more information, please see the [OOUI documentation on MediaWiki][1].
7089  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7091  *     @example
7092  *     // A select widget with three options.
7093  *     const select = new OO.ui.SelectWidget( {
7094  *         items: [
7095  *             new OO.ui.OptionWidget( {
7096  *                 data: 'a',
7097  *                 label: 'Option One',
7098  *             } ),
7099  *             new OO.ui.OptionWidget( {
7100  *                 data: 'b',
7101  *                 label: 'Option Two',
7102  *             } ),
7103  *             new OO.ui.OptionWidget( {
7104  *                 data: 'c',
7105  *                 label: 'Option Three',
7106  *             } )
7107  *         ]
7108  *     } );
7109  *     $( document.body ).append( select.$element );
7111  * @abstract
7112  * @class
7113  * @extends OO.ui.Widget
7114  * @mixes OO.ui.mixin.GroupWidget
7116  * @constructor
7117  * @param {Object} [config] Configuration options
7118  * @param {OO.ui.OptionWidget[]} [config.items] An array of options to add to the select.
7119  *  Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
7120  *  the [OOUI documentation on MediaWiki][2] for examples.
7121  *  [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7122  * @param {boolean} [config.multiselect=false] Allow for multiple selections
7123  */
7124 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
7125         // Configuration initialization
7126         config = config || {};
7128         // Parent constructor
7129         OO.ui.SelectWidget.super.call( this, config );
7131         // Mixin constructors
7132         OO.ui.mixin.GroupWidget.call( this, Object.assign( {
7133                 $group: this.$element
7134         }, config ) );
7136         // Properties
7137         this.pressed = false;
7138         this.selecting = null;
7139         this.multiselect = !!config.multiselect;
7140         this.onDocumentMouseUpHandler = this.onDocumentMouseUp.bind( this );
7141         this.onDocumentMouseMoveHandler = this.onDocumentMouseMove.bind( this );
7142         this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
7143         this.onDocumentKeyPressHandler = this.onDocumentKeyPress.bind( this );
7144         this.keyPressBuffer = '';
7145         this.keyPressBufferTimer = null;
7146         this.blockMouseOverEvents = 0;
7148         // Events
7149         this.connect( this, {
7150                 toggle: 'onToggle'
7151         } );
7152         this.$element.on( {
7153                 focusin: this.onFocus.bind( this ),
7154                 mousedown: this.onMouseDown.bind( this ),
7155                 mouseover: this.onMouseOver.bind( this ),
7156                 mouseleave: this.onMouseLeave.bind( this )
7157         } );
7159         // Initialization
7160         this.$element
7161                 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-unpressed' )
7162                 .attr( {
7163                         role: 'listbox',
7164                         'aria-multiselectable': this.multiselect.toString()
7165                 } );
7166         this.setFocusOwner( this.$element );
7167         this.addItems( config.items || [] );
7170 /* Setup */
7172 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
7173 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
7175 /* Events */
7178  * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
7180  * @event OO.ui.SelectWidget#highlight
7181  * @param {OO.ui.OptionWidget|null} item Highlighted item
7182  */
7185  * A `press` event is emitted when the #pressItem method is used to programmatically modify the
7186  * pressed state of an option.
7188  * @event OO.ui.SelectWidget#press
7189  * @param {OO.ui.OptionWidget|null} item Pressed item
7190  */
7193  * A `select` event is emitted when the selection is modified programmatically with the #selectItem
7194  * method.
7196  * @event OO.ui.SelectWidget#select
7197  * @param {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} items Currently selected items
7198  */
7201  * A `choose` event is emitted when an item is chosen with the #chooseItem method.
7203  * @event OO.ui.SelectWidget#choose
7204  * @param {OO.ui.OptionWidget} item Chosen item
7205  * @param {boolean} selected Item is selected
7206  */
7209  * An `add` event is emitted when options are added to the select with the #addItems method.
7211  * @event OO.ui.SelectWidget#add
7212  * @param {OO.ui.OptionWidget[]} items Added items
7213  * @param {number} index Index of insertion point
7214  */
7217  * A `remove` event is emitted when options are removed from the select with the #clearItems
7218  * or #removeItems methods.
7220  * @event OO.ui.SelectWidget#remove
7221  * @param {OO.ui.OptionWidget[]} items Removed items
7222  */
7224 /* Static Properties */
7227  * Whether this widget will respond to the navigation keys Home, End, PageUp, PageDown.
7229  * @static
7230  * @property {boolean}
7231  */
7232 OO.ui.SelectWidget.static.handleNavigationKeys = false;
7235  * Whether selecting items using arrow keys or navigation keys in this widget will wrap around after
7236  * the user reaches the beginning or end of the list.
7238  * @static
7239  * @property {boolean}
7240  */
7241 OO.ui.SelectWidget.static.listWrapsAround = true;
7243 /* Static methods */
7246  * Normalize text for filter matching
7248  * @param {string} text Text
7249  * @return {string} Normalized text
7250  */
7251 OO.ui.SelectWidget.static.normalizeForMatching = function ( text ) {
7252         // Replace trailing whitespace, normalize multiple spaces and make case insensitive
7253         let normalized = text.trim().replace( /\s+/, ' ' ).toLowerCase();
7255         // Normalize Unicode
7256         normalized = normalized.normalize();
7258         return normalized;
7261 /* Methods */
7264  * Handle focus events
7266  * @private
7267  * @param {jQuery.Event} event
7268  */
7269 OO.ui.SelectWidget.prototype.onFocus = function ( event ) {
7270         let item;
7271         if ( event.target === this.$element[ 0 ] ) {
7272                 // This widget was focussed, e.g. by the user tabbing to it.
7273                 // The styles for focus state depend on one of the items being selected.
7274                 if ( !this.findFirstSelectedItem() ) {
7275                         item = this.findFirstSelectableItem();
7276                 }
7277         } else {
7278                 if ( event.target.tabIndex === -1 ) {
7279                         // One of the options got focussed (and the event bubbled up here).
7280                         // They can't be tabbed to, but they can be activated using access keys.
7281                         // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
7282                         item = this.findTargetItem( event );
7283                         if ( item && !( item.isHighlightable() || item.isSelectable() ) ) {
7284                                 // The item is disabled (weirdly, disabled items can be focussed in Firefox and IE,
7285                                 // but not in Chrome). Do nothing (do not highlight or select anything).
7286                                 return;
7287                         }
7288                 } else {
7289                         // There is something actually user-focusable in one of the labels of the options, and
7290                         // the user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change
7291                         // the focus).
7292                         return;
7293                 }
7294         }
7296         if ( item ) {
7297                 if ( item.constructor.static.highlightable ) {
7298                         this.highlightItem( item );
7299                 } else {
7300                         this.selectItem( item );
7301                 }
7302         }
7304         if ( event.target !== this.$element[ 0 ] ) {
7305                 this.$focusOwner.trigger( 'focus' );
7306         }
7310  * Handle mouse down events.
7312  * @private
7313  * @param {jQuery.Event} e Mouse down event
7314  * @return {undefined|boolean} False to prevent default if event is handled
7315  */
7316 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
7317         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
7318                 this.togglePressed( true );
7319                 const item = this.findTargetItem( e );
7320                 if ( item && item.isSelectable() ) {
7321                         this.pressItem( item );
7322                         this.selecting = item;
7323                         this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler, true );
7324                         this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler, true );
7325                 }
7326         }
7327         return false;
7331  * Handle document mouse up events.
7333  * @private
7334  * @param {MouseEvent} e Mouse up event
7335  * @return {undefined|boolean} False to prevent default if event is handled
7336  */
7337 OO.ui.SelectWidget.prototype.onDocumentMouseUp = function ( e ) {
7338         this.togglePressed( false );
7339         if ( !this.selecting ) {
7340                 const item = this.findTargetItem( e );
7341                 if ( item && item.isSelectable() ) {
7342                         this.selecting = item;
7343                 }
7344         }
7345         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT && this.selecting ) {
7346                 this.pressItem( null );
7347                 this.chooseItem( this.selecting );
7348                 this.selecting = null;
7349         }
7351         this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler, true );
7352         this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler, true );
7354         return false;
7358  * Handle document mouse move events.
7360  * @private
7361  * @param {MouseEvent} e Mouse move event
7362  */
7363 OO.ui.SelectWidget.prototype.onDocumentMouseMove = function ( e ) {
7364         if ( !this.isDisabled() && this.pressed ) {
7365                 const item = this.findTargetItem( e );
7366                 if ( item && item !== this.selecting && item.isSelectable() ) {
7367                         this.pressItem( item );
7368                         this.selecting = item;
7369                 }
7370         }
7374  * Handle mouse over events.
7376  * @private
7377  * @param {jQuery.Event} e Mouse over event
7378  * @return {undefined|boolean} False to prevent default if event is handled
7379  */
7380 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
7381         if ( this.blockMouseOverEvents ) {
7382                 return;
7383         }
7384         if ( !this.isDisabled() ) {
7385                 const item = this.findTargetItem( e );
7386                 this.highlightItem( item && item.isHighlightable() ? item : null );
7387         }
7388         return false;
7392  * Handle mouse leave events.
7394  * @private
7395  * @param {jQuery.Event} e Mouse over event
7396  * @return {undefined|boolean} False to prevent default if event is handled
7397  */
7398 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
7399         if ( !this.isDisabled() ) {
7400                 this.highlightItem( null );
7401         }
7402         return false;
7406  * Handle document key down events.
7408  * @protected
7409  * @param {KeyboardEvent} e Key down event
7410  */
7411 OO.ui.SelectWidget.prototype.onDocumentKeyDown = function ( e ) {
7412         let handled = false;
7414         const currentItem =
7415                 ( this.isVisible() && this.findHighlightedItem() ) ||
7416                 ( !this.multiselect && this.findSelectedItem() );
7418         let nextItem;
7419         if ( !this.isDisabled() ) {
7420                 switch ( e.keyCode ) {
7421                         case OO.ui.Keys.ENTER:
7422                                 if ( currentItem ) {
7423                                         // Select highlighted item or toggle when multiselect is enabled
7424                                         this.chooseItem( currentItem );
7425                                         handled = true;
7426                                 }
7427                                 break;
7428                         case OO.ui.Keys.UP:
7429                         case OO.ui.Keys.LEFT:
7430                         case OO.ui.Keys.DOWN:
7431                         case OO.ui.Keys.RIGHT:
7432                                 this.clearKeyPressBuffer();
7433                                 nextItem = this.findRelativeSelectableItem(
7434                                         currentItem,
7435                                         e.keyCode === OO.ui.Keys.UP || e.keyCode === OO.ui.Keys.LEFT ? -1 : 1,
7436                                         null,
7437                                         this.constructor.static.listWrapsAround
7438                                 );
7439                                 handled = true;
7440                                 break;
7441                         case OO.ui.Keys.HOME:
7442                         case OO.ui.Keys.END:
7443                                 if ( this.constructor.static.handleNavigationKeys ) {
7444                                         this.clearKeyPressBuffer();
7445                                         nextItem = this.findRelativeSelectableItem(
7446                                                 null,
7447                                                 e.keyCode === OO.ui.Keys.HOME ? 1 : -1,
7448                                                 null,
7449                                                 this.constructor.static.listWrapsAround
7450                                         );
7451                                         handled = true;
7452                                 }
7453                                 break;
7454                         case OO.ui.Keys.PAGEUP:
7455                         case OO.ui.Keys.PAGEDOWN:
7456                                 if ( this.constructor.static.handleNavigationKeys ) {
7457                                         this.clearKeyPressBuffer();
7458                                         nextItem = this.findRelativeSelectableItem(
7459                                                 currentItem,
7460                                                 e.keyCode === OO.ui.Keys.PAGEUP ? -10 : 10,
7461                                                 null,
7462                                                 this.constructor.static.listWrapsAround
7463                                         );
7464                                         handled = true;
7465                                 }
7466                                 break;
7467                         case OO.ui.Keys.ESCAPE:
7468                         case OO.ui.Keys.TAB:
7469                                 if ( currentItem ) {
7470                                         currentItem.setHighlighted( false );
7471                                 }
7472                                 this.unbindDocumentKeyDownListener();
7473                                 this.unbindDocumentKeyPressListener();
7474                                 // Don't prevent tabbing away / defocusing
7475                                 handled = false;
7476                                 break;
7477                 }
7479                 if ( nextItem ) {
7480                         if ( this.isVisible() && nextItem.constructor.static.highlightable ) {
7481                                 this.highlightItem( nextItem );
7482                         } else {
7483                                 if ( this.screenReaderMode ) {
7484                                         this.highlightItem( nextItem );
7485                                 }
7486                                 this.chooseItem( nextItem );
7487                         }
7488                         this.scrollItemIntoView( nextItem );
7489                 }
7491                 if ( handled ) {
7492                         e.preventDefault();
7493                         e.stopPropagation();
7494                 }
7495         }
7499  * Bind document key down listener.
7501  * @protected
7502  */
7503 OO.ui.SelectWidget.prototype.bindDocumentKeyDownListener = function () {
7504         this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
7508  * Unbind document key down listener.
7510  * @protected
7511  */
7512 OO.ui.SelectWidget.prototype.unbindDocumentKeyDownListener = function () {
7513         this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
7517  * Scroll item into view, preventing spurious mouse highlight actions from happening.
7519  * @param {OO.ui.OptionWidget} item Item to scroll into view
7520  */
7521 OO.ui.SelectWidget.prototype.scrollItemIntoView = function ( item ) {
7522         // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic
7523         // scrolling and around 100-150 ms after it is finished.
7524         this.blockMouseOverEvents++;
7525         item.scrollElementIntoView().done( () => {
7526                 setTimeout( () => {
7527                         this.blockMouseOverEvents--;
7528                 }, 200 );
7529         } );
7533  * Clear the key-press buffer
7535  * @protected
7536  */
7537 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
7538         if ( this.keyPressBufferTimer ) {
7539                 clearTimeout( this.keyPressBufferTimer );
7540                 this.keyPressBufferTimer = null;
7541         }
7542         this.keyPressBuffer = '';
7546  * Handle key press events.
7548  * @protected
7549  * @param {KeyboardEvent} e Key press event
7550  * @return {undefined|boolean} False to prevent default if event is handled
7551  */
7552 OO.ui.SelectWidget.prototype.onDocumentKeyPress = function ( e ) {
7553         if ( !e.charCode ) {
7554                 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
7555                         this.keyPressBuffer = this.keyPressBuffer.slice( 0, this.keyPressBuffer.length - 1 );
7556                         return false;
7557                 }
7558                 return;
7559         }
7561         const c = String.fromCodePoint( e.charCode );
7563         if ( this.keyPressBufferTimer ) {
7564                 clearTimeout( this.keyPressBufferTimer );
7565         }
7566         this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
7568         let item = ( this.isVisible() && this.findHighlightedItem() ) ||
7569                 ( !this.multiselect && this.findSelectedItem() );
7571         if ( this.keyPressBuffer === c ) {
7572                 // Common (if weird) special case: typing "xxxx" will cycle through all
7573                 // the items beginning with "x".
7574                 if ( item ) {
7575                         item = this.findRelativeSelectableItem( item, 1 );
7576                 }
7577         } else {
7578                 this.keyPressBuffer += c;
7579         }
7581         const filter = this.getItemMatcher( this.keyPressBuffer, false );
7582         if ( !item || !filter( item ) ) {
7583                 item = this.findRelativeSelectableItem( item, 1, filter );
7584         }
7585         if ( item ) {
7586                 if ( this.isVisible() && item.constructor.static.highlightable ) {
7587                         this.highlightItem( item );
7588                 } else {
7589                         if ( this.screenReaderMode ) {
7590                                 this.highlightItem( item );
7591                         }
7592                         this.chooseItem( item );
7593                 }
7594                 this.scrollItemIntoView( item );
7595         }
7597         e.preventDefault();
7598         e.stopPropagation();
7602  * Get a matcher for the specific string
7604  * @protected
7605  * @param {string} query String to match against items
7606  * @param {string} [mode='prefix'] Matching mode: 'substring', 'prefix', or 'exact'
7607  * @return {Function} function ( OO.ui.OptionWidget ) => boolean
7608  */
7609 OO.ui.SelectWidget.prototype.getItemMatcher = function ( query, mode ) {
7610         const normalizeForMatching = this.constructor.static.normalizeForMatching,
7611                 normalizedQuery = normalizeForMatching( query );
7613         // Empty string matches everything, except in "exact" mode where it matches nothing
7614         if ( !normalizedQuery ) {
7615                 return function () {
7616                         return mode !== 'exact';
7617                 };
7618         }
7620         return function ( item ) {
7621                 const matchText = normalizeForMatching( item.getMatchText() );
7622                 switch ( mode ) {
7623                         case 'exact':
7624                                 return matchText === normalizedQuery;
7625                         case 'substring':
7626                                 return matchText.indexOf( normalizedQuery ) !== -1;
7627                         // 'prefix'
7628                         default:
7629                                 return matchText.indexOf( normalizedQuery ) === 0;
7630                 }
7631         };
7635  * Bind document key press listener.
7637  * @protected
7638  */
7639 OO.ui.SelectWidget.prototype.bindDocumentKeyPressListener = function () {
7640         this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler, true );
7644  * Unbind document key down listener.
7646  * If you override this, be sure to call this.clearKeyPressBuffer() from your
7647  * implementation.
7649  * @protected
7650  */
7651 OO.ui.SelectWidget.prototype.unbindDocumentKeyPressListener = function () {
7652         this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler, true );
7653         this.clearKeyPressBuffer();
7657  * Visibility change handler
7659  * @protected
7660  * @param {boolean} visible
7661  */
7662 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
7663         if ( !visible ) {
7664                 this.clearKeyPressBuffer();
7665         }
7669  * Get the closest item to a jQuery.Event.
7671  * @private
7672  * @param {jQuery.Event} e
7673  * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
7674  */
7675 OO.ui.SelectWidget.prototype.findTargetItem = function ( e ) {
7676         const $option = $( e.target ).closest( '.oo-ui-optionWidget' );
7677         if ( !$option.closest( '.oo-ui-selectWidget' ).is( this.$element ) ) {
7678                 return null;
7679         }
7680         return $option.data( 'oo-ui-optionWidget' ) || null;
7684  * @return {OO.ui.OptionWidget|null} The first (of possibly many) selected item, if any
7685  */
7686 OO.ui.SelectWidget.prototype.findFirstSelectedItem = function () {
7687         for ( let i = 0; i < this.items.length; i++ ) {
7688                 if ( this.items[ i ].isSelected() ) {
7689                         return this.items[ i ];
7690                 }
7691         }
7692         return null;
7696  * Find all selected items, if there are any. If the widget allows for multiselect
7697  * it will return an array of selected options. If the widget doesn't allow for
7698  * multiselect, it will return the selected option or null if no item is selected.
7700  * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
7701  *  then return an array of selected items (or empty array),
7702  *  if the widget is not multiselect, return a single selected item, or `null`
7703  *  if no item is selected
7704  */
7705 OO.ui.SelectWidget.prototype.findSelectedItems = function () {
7706         if ( !this.multiselect ) {
7707                 return this.findFirstSelectedItem();
7708         }
7710         return this.items.filter( ( item ) => item.isSelected() );
7714  * Find selected item.
7716  * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
7717  *  then return an array of selected items (or empty array),
7718  *  if the widget is not multiselect, return a single selected item, or `null`
7719  *  if no item is selected
7720  */
7721 OO.ui.SelectWidget.prototype.findSelectedItem = function () {
7722         return this.findSelectedItems();
7726  * Find highlighted item.
7728  * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
7729  */
7730 OO.ui.SelectWidget.prototype.findHighlightedItem = function () {
7731         for ( let i = 0; i < this.items.length; i++ ) {
7732                 if ( this.items[ i ].isHighlighted() ) {
7733                         return this.items[ i ];
7734                 }
7735         }
7736         return null;
7740  * Toggle pressed state.
7742  * Press is a state that occurs when a user mouses down on an item, but
7743  * has not yet let go of the mouse. The item may appear selected, but it will not be selected
7744  * until the user releases the mouse.
7746  * @param {boolean} [pressed] An option is being pressed, omit to toggle
7747  */
7748 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
7749         if ( pressed === undefined ) {
7750                 pressed = !this.pressed;
7751         }
7752         if ( pressed !== this.pressed ) {
7753                 this.$element
7754                         .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
7755                         .toggleClass( 'oo-ui-selectWidget-unpressed', !pressed );
7756                 this.pressed = pressed;
7757         }
7761  * Highlight an option. If the `item` param is omitted, no options will be highlighted
7762  * and any existing highlight will be removed. The highlight is mutually exclusive.
7764  * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
7765  * @fires OO.ui.SelectWidget#highlight
7766  * @chainable
7767  * @return {OO.ui.Widget} The widget, for chaining
7768  */
7769 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
7770         if ( item && item.isHighlighted() ) {
7771                 return this;
7772         }
7774         let changed = false;
7776         for ( let i = 0; i < this.items.length; i++ ) {
7777                 const highlighted = this.items[ i ] === item;
7778                 if ( this.items[ i ].isHighlighted() !== highlighted ) {
7779                         this.items[ i ].setHighlighted( highlighted );
7780                         if ( changed ) {
7781                                 // This was the second change; there can only be two, a set and an unset
7782                                 break;
7783                         }
7784                         // Un-highlighting can't fail, but highlighting can
7785                         changed = !highlighted || this.items[ i ].isHighlighted();
7786                 }
7787         }
7789         if ( changed ) {
7790                 if ( item ) {
7791                         this.$focusOwner.attr( 'aria-activedescendant', item.getElementId() );
7792                 } else {
7793                         this.$focusOwner.removeAttr( 'aria-activedescendant' );
7794                 }
7795                 this.emit( 'highlight', item );
7796         }
7798         return this;
7802  * Fetch an item by its label.
7804  * @param {string} label Label of the item to select.
7805  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7806  * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7807  */
7808 OO.ui.SelectWidget.prototype.getItemFromLabel = function ( label, prefix ) {
7809         const len = this.items.length;
7811         let filter = this.getItemMatcher( label, 'exact' );
7813         let i, item;
7814         for ( i = 0; i < len; i++ ) {
7815                 item = this.items[ i ];
7816                 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
7817                         return item;
7818                 }
7819         }
7821         if ( prefix ) {
7822                 let found = null;
7823                 filter = this.getItemMatcher( label, 'prefix' );
7824                 for ( i = 0; i < len; i++ ) {
7825                         item = this.items[ i ];
7826                         if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
7827                                 if ( found ) {
7828                                         return null;
7829                                 }
7830                                 found = item;
7831                         }
7832                 }
7833                 if ( found ) {
7834                         return found;
7835                 }
7836         }
7838         return null;
7842  * Programmatically select an option by its label. If the item does not exist,
7843  * all options will be deselected.
7845  * @param {string} [label] Label of the item to select.
7846  * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7847  * @fires OO.ui.SelectWidget#select
7848  * @chainable
7849  * @return {OO.ui.Widget} The widget, for chaining
7850  */
7851 OO.ui.SelectWidget.prototype.selectItemByLabel = function ( label, prefix ) {
7852         const itemFromLabel = this.getItemFromLabel( label, !!prefix );
7853         if ( label === undefined || !itemFromLabel ) {
7854                 return this.selectItem();
7855         }
7856         return this.selectItem( itemFromLabel );
7860  * Programmatically select an option by its data. If the `data` parameter is omitted,
7861  * or if the item does not exist, all options will be deselected.
7863  * @param {Object|string} [data] Value of the item to select, omit to deselect all
7864  * @fires OO.ui.SelectWidget#select
7865  * @chainable
7866  * @return {OO.ui.Widget} The widget, for chaining
7867  */
7868 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
7869         const itemFromData = this.findItemFromData( data );
7870         if ( data === undefined || !itemFromData ) {
7871                 return this.selectItem();
7872         }
7873         return this.selectItem( itemFromData );
7877  * Programmatically unselect an option by its reference. If the widget
7878  * allows for multiple selections, there may be other items still selected;
7879  * otherwise, no items will be selected.
7880  * If no item is given, all selected items will be unselected.
7882  * @param {OO.ui.OptionWidget} [unselectedItem] Item to unselect, or nothing to unselect all
7883  * @fires OO.ui.SelectWidget#select
7884  * @chainable
7885  * @return {OO.ui.Widget} The widget, for chaining
7886  */
7887 OO.ui.SelectWidget.prototype.unselectItem = function ( unselectedItem ) {
7888         if ( !unselectedItem ) {
7889                 // Unselect all
7890                 this.selectItem();
7891         } else if ( unselectedItem.isSelected() ) {
7892                 unselectedItem.setSelected( false );
7893                 // Other items might still be selected in multiselect mode
7894                 this.emit( 'select', this.findSelectedItems() );
7895         }
7897         return this;
7901  * Programmatically select an option by its reference. If the `item` parameter is omitted,
7902  * all options will be deselected.
7904  * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7905  * @fires OO.ui.SelectWidget#select
7906  * @chainable
7907  * @return {OO.ui.Widget} The widget, for chaining
7908  */
7909 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
7910         if ( item ) {
7911                 if ( item.isSelected() ) {
7912                         return this;
7913                 } else if ( this.multiselect ) {
7914                         // We don't care about the state of the other items when multiselect is allowed
7915                         item.setSelected( true );
7916                         this.emit( 'select', this.findSelectedItems() );
7917                         return this;
7918                 }
7919         }
7921         let changed = false;
7923         for ( let i = 0; i < this.items.length; i++ ) {
7924                 const selected = this.items[ i ] === item;
7925                 if ( this.items[ i ].isSelected() !== selected ) {
7926                         this.items[ i ].setSelected( selected );
7927                         if ( changed && !this.multiselect ) {
7928                                 // This was the second change; there can only be two, a set and an unset
7929                                 break;
7930                         }
7931                         // Un-selecting can't fail, but selecting can
7932                         changed = !selected || this.items[ i ].isSelected();
7933                 }
7934         }
7936         if ( changed ) {
7937                 // Fall back to the selected instead of the highlighted option (see #highlightItem) only
7938                 // when we know highlighting is disabled. Unfortunately we can't know without an item.
7939                 // Don't even try when an arbitrary number of options can be selected.
7940                 if ( !this.multiselect && item && !item.constructor.static.highlightable ) {
7941                         this.$focusOwner.attr( 'aria-activedescendant', item.getElementId() );
7942                 }
7943                 this.emit( 'select', this.findSelectedItems() );
7944         }
7946         return this;
7950  * Press an item.
7952  * Press is a state that occurs when a user mouses down on an item, but has not
7953  * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7954  * releases the mouse.
7956  * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7957  * @fires OO.ui.SelectWidget#press
7958  * @chainable
7959  * @return {OO.ui.Widget} The widget, for chaining
7960  */
7961 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
7962         if ( item && item.isPressed() ) {
7963                 return this;
7964         }
7966         let changed = false;
7968         for ( let i = 0; i < this.items.length; i++ ) {
7969                 const pressed = this.items[ i ] === item;
7970                 if ( this.items[ i ].isPressed() !== pressed ) {
7971                         this.items[ i ].setPressed( pressed );
7972                         if ( changed ) {
7973                                 // This was the second change; there can only be two, a set and an unset
7974                                 break;
7975                         }
7976                         // Un-pressing can't fail, but pressing can
7977                         changed = !pressed || this.items[ i ].isPressed();
7978                 }
7979         }
7981         if ( changed ) {
7982                 this.emit( 'press', item );
7983         }
7985         return this;
7989  * Select an item or toggle an item's selection when multiselect is enabled.
7991  * Note that ‘choose’ should never be modified programmatically. A user can choose
7992  * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7993  * use the #selectItem method.
7995  * This method is not identical to #selectItem and may vary further in subclasses that take
7996  * additional action when users choose an item with the keyboard or mouse.
7998  * @param {OO.ui.OptionWidget} item Item to choose
7999  * @fires OO.ui.SelectWidget#choose
8000  * @chainable
8001  * @return {OO.ui.Widget} The widget, for chaining
8002  */
8003 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
8004         if ( item ) {
8005                 if ( this.multiselect && item.isSelected() ) {
8006                         this.unselectItem( item );
8007                 } else {
8008                         this.selectItem( item );
8009                 }
8011                 this.emit( 'choose', item, item.isSelected() );
8012         }
8014         return this;
8018  * Find an option by its position relative to the specified item (or to the start of the option
8019  * array, if item is `null`). The direction and distance in which to search through the option array
8020  * is specified with a number: e.g. -1 for the previous item (the default) or 1 for the next item,
8021  * or 15 for the 15th next item, etc. The method will return an option, or `null` if there are no
8022  * options in the array.
8024  * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at
8025  *  the beginning of the array.
8026  * @param {number} offset Relative position: negative to move backward, positive to move forward
8027  * @param {Function} [filter] Only consider items for which this function returns
8028  *  true. Function takes an OO.ui.OptionWidget and returns a boolean.
8029  * @param {boolean} [wrap=false] Do not wrap around after reaching the last or first item
8030  * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
8031  */
8032 OO.ui.SelectWidget.prototype.findRelativeSelectableItem = function ( item, offset, filter, wrap ) {
8033         const step = offset > 0 ? 1 : -1,
8034                 len = this.items.length;
8035         if ( wrap === undefined ) {
8036                 wrap = true;
8037         }
8039         let nextIndex;
8040         if ( item instanceof OO.ui.OptionWidget ) {
8041                 nextIndex = this.items.indexOf( item );
8042         } else {
8043                 // If no item is selected and moving forward, start at the beginning.
8044                 // If moving backward, start at the end.
8045                 nextIndex = offset > 0 ? 0 : len - 1;
8046                 offset -= step;
8047         }
8049         const previousItem = item;
8050         let nextItem = null;
8051         for ( let i = 0; i < len; i++ ) {
8052                 item = this.items[ nextIndex ];
8053                 if (
8054                         item instanceof OO.ui.OptionWidget && item.isSelectable() &&
8055                         ( !filter || filter( item ) )
8056                 ) {
8057                         nextItem = item;
8058                 }
8060                 if ( offset === 0 && nextItem && nextItem !== previousItem ) {
8061                         // We walked at least the desired number of steps *and* we've selected a different item.
8062                         // This is to ensure that disabled items don't cause us to get stuck or return null.
8063                         break;
8064                 }
8066                 nextIndex += step;
8067                 if ( nextIndex < 0 || nextIndex >= len ) {
8068                         if ( wrap ) {
8069                                 nextIndex = ( nextIndex + len ) % len;
8070                         } else {
8071                                 // We ran out of the list, return whichever was the last valid item
8072                                 break;
8073                         }
8074                 }
8075                 if ( offset !== 0 ) {
8076                         offset -= step;
8077                 }
8078         }
8079         return nextItem;
8083  * Find the next selectable item or `null` if there are no selectable items.
8084  * Disabled options and menu-section markers and breaks are not selectable.
8086  * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
8087  */
8088 OO.ui.SelectWidget.prototype.findFirstSelectableItem = function () {
8089         return this.findRelativeSelectableItem( null, 1 );
8093  * Add an array of options to the select. Optionally, an index number can be used to
8094  * specify an insertion point.
8096  * @param {OO.ui.OptionWidget[]} [items] Options to add
8097  * @param {number} [index] Index to insert items after
8098  * @fires OO.ui.SelectWidget#add
8099  * @chainable
8100  * @return {OO.ui.Widget} The widget, for chaining
8101  */
8102 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
8103         if ( !items || items.length === 0 ) {
8104                 return this;
8105         }
8107         // Mixin method
8108         OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
8110         // Always provide an index, even if it was omitted
8111         this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
8113         return this;
8117  * Remove the specified array of options from the select. Options will be detached
8118  * from the DOM, not removed, so they can be reused later. To remove all options from
8119  * the select, you may wish to use the #clearItems method instead.
8121  * @param {OO.ui.OptionWidget[]} items Items to remove
8122  * @fires OO.ui.SelectWidget#remove
8123  * @chainable
8124  * @return {OO.ui.Widget} The widget, for chaining
8125  */
8126 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
8127         // Deselect items being removed
8128         for ( let i = 0; i < items.length; i++ ) {
8129                 const item = items[ i ];
8130                 if ( item.isSelected() ) {
8131                         this.selectItem( null );
8132                 }
8133         }
8135         // Mixin method
8136         OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
8138         this.emit( 'remove', items );
8140         return this;
8144  * Clear all options from the select. Options will be detached from the DOM, not removed,
8145  * so that they can be reused later. To remove a subset of options from the select, use
8146  * the #removeItems method.
8148  * @fires OO.ui.SelectWidget#remove
8149  * @chainable
8150  * @return {OO.ui.Widget} The widget, for chaining
8151  */
8152 OO.ui.SelectWidget.prototype.clearItems = function () {
8153         const items = this.items.slice();
8155         // Mixin method
8156         OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
8158         // Clear selection
8159         this.selectItem( null );
8161         this.emit( 'remove', items );
8163         return this;
8167  * Set the DOM element which has focus while the user is interacting with this SelectWidget.
8169  * This is used to set `aria-activedescendant` and `aria-expanded` on it.
8171  * @protected
8172  * @param {jQuery} $focusOwner
8173  */
8174 OO.ui.SelectWidget.prototype.setFocusOwner = function ( $focusOwner ) {
8175         this.$focusOwner = $focusOwner;
8179  * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
8180  * with an {@link OO.ui.mixin.IconElement icon} and/or
8181  * {@link OO.ui.mixin.IndicatorElement indicator}.
8182  * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
8183  * options. For more information about options and selects, please see the
8184  * [OOUI documentation on MediaWiki][1].
8186  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8188  *     @example
8189  *     // Decorated options in a select widget.
8190  *     const select = new OO.ui.SelectWidget( {
8191  *         items: [
8192  *             new OO.ui.DecoratedOptionWidget( {
8193  *                 data: 'a',
8194  *                 label: 'Option with icon',
8195  *                 icon: 'help'
8196  *             } ),
8197  *             new OO.ui.DecoratedOptionWidget( {
8198  *                 data: 'b',
8199  *                 label: 'Option with indicator',
8200  *                 indicator: 'next'
8201  *             } )
8202  *         ]
8203  *     } );
8204  *     $( document.body ).append( select.$element );
8206  * @class
8207  * @extends OO.ui.OptionWidget
8208  * @mixes OO.ui.mixin.IconElement
8209  * @mixes OO.ui.mixin.IndicatorElement
8211  * @constructor
8212  * @param {Object} [config] Configuration options
8213  */
8214 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
8215         // Parent constructor
8216         OO.ui.DecoratedOptionWidget.super.call( this, config );
8218         // Mixin constructors
8219         OO.ui.mixin.IconElement.call( this, config );
8220         OO.ui.mixin.IndicatorElement.call( this, config );
8222         // Initialization
8223         this.$element
8224                 .addClass( 'oo-ui-decoratedOptionWidget' )
8225                 .prepend( this.$icon )
8226                 .append( this.$indicator );
8229 /* Setup */
8231 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
8232 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
8233 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
8236  * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
8237  * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
8238  * the [OOUI documentation on MediaWiki][1] for more information.
8240  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8242  * @class
8243  * @extends OO.ui.DecoratedOptionWidget
8245  * @constructor
8246  * @param {Object} [config] Configuration options
8247  */
8248 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
8249         // Parent constructor
8250         OO.ui.MenuOptionWidget.super.call( this, config );
8252         // Properties
8253         this.checkIcon = new OO.ui.IconWidget( {
8254                 icon: 'check',
8255                 classes: [ 'oo-ui-menuOptionWidget-checkIcon' ]
8256         } );
8258         // Initialization
8259         this.$element
8260                 .prepend( this.checkIcon.$element )
8261                 .addClass( 'oo-ui-menuOptionWidget' );
8264 /* Setup */
8266 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
8268 /* Static Properties */
8271  * @static
8272  * @inheritdoc
8273  */
8274 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
8277  * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to
8278  * group one or more related {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets
8279  * cannot be highlighted or selected.
8281  *     @example
8282  *     const dropdown = new OO.ui.DropdownWidget( {
8283  *         menu: {
8284  *             items: [
8285  *                 new OO.ui.MenuSectionOptionWidget( {
8286  *                     label: 'Dogs'
8287  *                 } ),
8288  *                 new OO.ui.MenuOptionWidget( {
8289  *                     data: 'corgi',
8290  *                     label: 'Welsh Corgi'
8291  *                 } ),
8292  *                 new OO.ui.MenuOptionWidget( {
8293  *                     data: 'poodle',
8294  *                     label: 'Standard Poodle'
8295  *                 } ),
8296  *                 new OO.ui.MenuSectionOptionWidget( {
8297  *                     label: 'Cats'
8298  *                 } ),
8299  *                 new OO.ui.MenuOptionWidget( {
8300  *                     data: 'lion',
8301  *                     label: 'Lion'
8302  *                 } )
8303  *             ]
8304  *         }
8305  *     } );
8306  *     $( document.body ).append( dropdown.$element );
8308  * @class
8309  * @extends OO.ui.DecoratedOptionWidget
8311  * @constructor
8312  * @param {Object} [config] Configuration options
8313  */
8314 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
8315         // Parent constructor
8316         OO.ui.MenuSectionOptionWidget.super.call( this, config );
8318         // Initialization
8319         this.$element
8320                 .addClass( 'oo-ui-menuSectionOptionWidget' )
8321                 .removeAttr( 'role aria-selected' );
8322         this.selected = false;
8325 /* Setup */
8327 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
8329 /* Static Properties */
8332  * @static
8333  * @inheritdoc
8334  */
8335 OO.ui.MenuSectionOptionWidget.static.selectable = false;
8338  * @static
8339  * @inheritdoc
8340  */
8341 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
8344  * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
8345  * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
8346  * See {@link OO.ui.DropdownWidget DropdownWidget},
8347  * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}, and
8348  * {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
8349  * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
8350  * and customized to be opened, closed, and displayed as needed.
8352  * By default, menus are clipped to the visible viewport and are not visible when a user presses the
8353  * mouse outside the menu.
8355  * Menus also have support for keyboard interaction:
8357  * - Enter/Return key: choose and select a menu option
8358  * - Up-arrow key: highlight the previous menu option
8359  * - Down-arrow key: highlight the next menu option
8360  * - Escape key: hide the menu
8362  * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
8364  * Please see the [OOUI documentation on MediaWiki][1] for more information.
8365  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8367  * @class
8368  * @extends OO.ui.SelectWidget
8369  * @mixes OO.ui.mixin.ClippableElement
8370  * @mixes OO.ui.mixin.FloatableElement
8372  * @constructor
8373  * @param {Object} [config] Configuration options
8374  * @param {OO.ui.TextInputWidget} [config.input] Text input used to implement option highlighting for menu
8375  *  items that match the text the user types. This config is used by
8376  *  {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget} and
8377  *  {@link OO.ui.mixin.LookupElement LookupElement}
8378  * @param {jQuery} [config.$input] Text input used to implement option highlighting for menu items that match
8379  *  the text the user types. This config is used by
8380  *  {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
8381  * @param {OO.ui.Widget} [config.widget] Widget associated with the menu's active state. If the user clicks
8382  *  the mouse anywhere on the page outside of this widget, the menu is hidden. For example, if
8383  *  there is a button that toggles the menu's visibility on click, the menu will be hidden then
8384  *  re-shown when the user clicks that button, unless the button (or its parent widget) is passed
8385  *  in here.
8386  * @param {boolean} [config.autoHide=true] Hide the menu when the mouse is pressed outside the menu.
8387  * @param {jQuery} [config.$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
8388  * @param {boolean} [config.hideOnChoose=true] Hide the menu when the user chooses an option.
8389  * @param {boolean} [config.filterFromInput=false] Filter the displayed options from the input
8390  * @param {boolean} [config.highlightOnFilter=false] Highlight the first result when filtering
8391  * @param {string} [config.filterMode='prefix'] The mode by which the menu filters the results.
8392  *  Options are 'exact', 'prefix' or 'substring'. See `OO.ui.SelectWidget#getItemMatcher`
8393  * @param {number|string} [config.width] Width of the menu as a number of pixels or CSS string with unit
8394  *  suffix, used by {@link OO.ui.mixin.ClippableElement ClippableElement}
8395  */
8396 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
8397         // Configuration initialization
8398         config = config || {};
8400         // Parent constructor
8401         OO.ui.MenuSelectWidget.super.call( this, config );
8403         // Mixin constructors
8404         OO.ui.mixin.ClippableElement.call( this, Object.assign( { $clippable: this.$group }, config ) );
8405         OO.ui.mixin.FloatableElement.call( this, config );
8407         // Initial vertical positions other than 'center' will result in
8408         // the menu being flipped if there is not enough space in the container.
8409         // Store the original position so we know what to reset to.
8410         this.originalVerticalPosition = this.verticalPosition;
8412         // Properties
8413         this.autoHide = config.autoHide === undefined || !!config.autoHide;
8414         this.hideOnChoose = config.hideOnChoose === undefined || !!config.hideOnChoose;
8415         this.filterFromInput = !!config.filterFromInput;
8416         this.previouslySelectedValue = null;
8417         this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
8418         this.$widget = config.widget ? config.widget.$element : null;
8419         this.$autoCloseIgnore = config.$autoCloseIgnore || $( [] );
8420         this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
8421         this.onInputEditHandler = OO.ui.debounce( this.updateItemVisibility.bind( this ), 100 );
8422         this.highlightOnFilter = !!config.highlightOnFilter;
8423         this.lastHighlightedItem = null;
8424         this.width = config.width;
8425         this.filterMode = config.filterMode;
8426         this.screenReaderMode = false;
8428         // Initialization
8429         this.$element.addClass( 'oo-ui-menuSelectWidget' );
8430         if ( config.widget ) {
8431                 this.setFocusOwner( config.widget.$tabIndexed );
8432         }
8434         // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
8435         // that reference properties not initialized at that time of parent class construction
8436         // TODO: Find a better way to handle post-constructor setup
8437         this.visible = false;
8438         this.$element.addClass( 'oo-ui-element-hidden' );
8439         this.$focusOwner.attr( 'aria-expanded', 'false' );
8442 /* Setup */
8444 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
8445 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
8446 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.FloatableElement );
8448 /* Events */
8451  * The menu is ready: it is visible and has been positioned and clipped.
8453  * @event OO.ui.MenuSelectWidget#ready
8454  */
8456 /* Static properties */
8458 OO.ui.MenuSelectWidget.static.handleNavigationKeys = true;
8460 OO.ui.MenuSelectWidget.static.listWrapsAround = false;
8463  * Positions to flip to if there isn't room in the container for the
8464  * menu in a specific direction.
8466  * @property {Object.<string,string>}
8467  */
8468 OO.ui.MenuSelectWidget.static.flippedPositions = {
8469         below: 'above',
8470         above: 'below',
8471         top: 'bottom',
8472         bottom: 'top'
8475 /* Methods */
8478  * Handles document mouse down events.
8480  * @protected
8481  * @param {MouseEvent} e Mouse down event
8482  */
8483 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
8484         if (
8485                 this.isVisible() &&
8486                 !OO.ui.contains(
8487                         this.$element.add( this.$widget ).add( this.$autoCloseIgnore ).get(),
8488                         e.target,
8489                         true
8490                 )
8491         ) {
8492                 this.toggle( false );
8493         }
8497  * @inheritdoc
8498  */
8499 OO.ui.MenuSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
8500         let handled = false;
8502         const currentItem = this.findHighlightedItem() || this.findFirstSelectedItem();
8504         if ( !this.isDisabled() && this.getVisibleItems().length ) {
8505                 switch ( e.keyCode ) {
8506                         case OO.ui.Keys.ENTER:
8507                                 if ( this.isVisible() ) {
8508                                         OO.ui.MenuSelectWidget.super.prototype.onDocumentKeyDown.call( this, e );
8509                                 }
8510                                 break;
8511                         case OO.ui.Keys.TAB:
8512                                 if ( this.isVisible() ) {
8513                                         if ( currentItem && !currentItem.isSelected() ) {
8514                                                 // Was only highlighted, now let's select it. No-op if already selected.
8515                                                 this.chooseItem( currentItem );
8516                                                 handled = true;
8517                                         }
8518                                         this.toggle( false );
8519                                 }
8520                                 break;
8521                         case OO.ui.Keys.LEFT:
8522                         case OO.ui.Keys.RIGHT:
8523                         case OO.ui.Keys.HOME:
8524                         case OO.ui.Keys.END:
8525                                 // Do nothing if a text field is associated, these keys will be handled by the
8526                                 // text input
8527                                 if ( !this.$input ) {
8528                                         OO.ui.MenuSelectWidget.super.prototype.onDocumentKeyDown.call( this, e );
8529                                 }
8530                                 break;
8531                         case OO.ui.Keys.ESCAPE:
8532                                 if ( this.isVisible() ) {
8533                                         if ( currentItem && !this.multiselect ) {
8534                                                 currentItem.setHighlighted( false );
8535                                         }
8536                                         this.toggle( false );
8537                                         handled = true;
8538                                 }
8539                                 break;
8540                         default:
8541                                 return OO.ui.MenuSelectWidget.super.prototype.onDocumentKeyDown.call( this, e );
8542                 }
8543                 if ( handled ) {
8544                         e.preventDefault();
8545                         e.stopPropagation();
8546                 }
8547         }
8551  * Return the visible items in the menu.
8553  * @return {OO.ui.MenuOptionWidget[]} Visible items
8554  */
8555 OO.ui.MenuSelectWidget.prototype.getVisibleItems = function () {
8556         return this.getItems().filter( ( item ) => item.isVisible() );
8560  * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
8561  * or after items were added/removed (always).
8563  * @protected
8564  */
8565 OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
8566         if ( !this.filterFromInput || !this.$input ) {
8567                 this.clip();
8568                 return;
8569         }
8571         let anyVisible = false;
8573         const showAll = !this.isVisible() || this.previouslySelectedValue === this.$input.val(),
8574                 filter = showAll ? null : this.getItemMatcher( this.$input.val(), this.filterMode );
8575         // Hide non-matching options, and also hide section headers if all options
8576         // in their section are hidden.
8577         let item;
8578         let section, sectionEmpty;
8579         for ( let i = 0; i < this.items.length; i++ ) {
8580                 item = this.items[ i ];
8581                 if ( item instanceof OO.ui.MenuSectionOptionWidget ) {
8582                         if ( section ) {
8583                                 // If the previous section was empty, hide its header
8584                                 section.toggle( showAll || !sectionEmpty );
8585                         }
8586                         section = item;
8587                         sectionEmpty = true;
8588                 } else if ( item instanceof OO.ui.OptionWidget ) {
8589                         const visible = !filter || filter( item );
8590                         anyVisible = anyVisible || visible;
8591                         sectionEmpty = sectionEmpty && !visible;
8592                         item.toggle( visible );
8593                 }
8594         }
8595         // Process the final section
8596         if ( section ) {
8597                 section.toggle( showAll || !sectionEmpty );
8598         }
8600         if ( !anyVisible ) {
8601                 this.highlightItem( null );
8602         }
8604         this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
8606         if ( this.highlightOnFilter &&
8607                 !( this.lastHighlightedItem && this.lastHighlightedItem.isSelectable() ) &&
8608                 this.isVisible()
8609         ) {
8610                 // Highlight the first selectable item in the list
8611                 item = this.findFirstSelectableItem();
8612                 this.highlightItem( item );
8613                 this.lastHighlightedItem = item;
8614         }
8616         // Reevaluate clipping
8617         this.clip();
8621  * @inheritdoc
8622  */
8623 OO.ui.MenuSelectWidget.prototype.bindDocumentKeyDownListener = function () {
8624         if ( this.$input ) {
8625                 this.$input.on( 'keydown', this.onDocumentKeyDownHandler );
8626         } else {
8627                 OO.ui.MenuSelectWidget.super.prototype.bindDocumentKeyDownListener.call( this );
8628         }
8632  * @inheritdoc
8633  */
8634 OO.ui.MenuSelectWidget.prototype.unbindDocumentKeyDownListener = function () {
8635         if ( this.$input ) {
8636                 this.$input.off( 'keydown', this.onDocumentKeyDownHandler );
8637         } else {
8638                 OO.ui.MenuSelectWidget.super.prototype.unbindDocumentKeyDownListener.call( this );
8639         }
8643  * @inheritdoc
8644  */
8645 OO.ui.MenuSelectWidget.prototype.bindDocumentKeyPressListener = function () {
8646         if ( this.$input ) {
8647                 if ( this.filterFromInput ) {
8648                         this.$input.on(
8649                                 'keydown mouseup cut paste change input select',
8650                                 this.onInputEditHandler
8651                         );
8652                         this.$input.one( 'keypress', () => {
8653                                 this.previouslySelectedValue = null;
8654                         } );
8655                         this.previouslySelectedValue = this.$input.val();
8656                         this.updateItemVisibility();
8657                 }
8658         } else {
8659                 OO.ui.MenuSelectWidget.super.prototype.bindDocumentKeyPressListener.call( this );
8660         }
8664  * @inheritdoc
8665  */
8666 OO.ui.MenuSelectWidget.prototype.unbindDocumentKeyPressListener = function () {
8667         if ( this.$input ) {
8668                 if ( this.filterFromInput ) {
8669                         this.$input.off(
8670                                 'keydown mouseup cut paste change input select',
8671                                 this.onInputEditHandler
8672                         );
8673                         this.updateItemVisibility();
8674                 }
8675         } else {
8676                 OO.ui.MenuSelectWidget.super.prototype.unbindDocumentKeyPressListener.call( this );
8677         }
8681  * Select an item or toggle an item's selection when multiselect is enabled.
8683  * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is
8684  * set to false.
8686  * Note that ‘choose’ should never be modified programmatically. A user can choose an option with
8687  * the keyboard or mouse and it becomes selected. To select an item programmatically,
8688  * use the #selectItem method.
8690  * @param {OO.ui.OptionWidget} item Item to choose
8691  * @chainable
8692  * @return {OO.ui.Widget} The widget, for chaining
8693  */
8694 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
8695         OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
8696         if ( this.hideOnChoose ) {
8697                 this.toggle( false );
8698         }
8699         return this;
8703  * @inheritdoc
8704  */
8705 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
8706         if ( !items || items.length === 0 ) {
8707                 return this;
8708         }
8710         // Parent method
8711         OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
8713         this.updateItemVisibility();
8715         return this;
8719  * @inheritdoc
8720  */
8721 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
8722         // Parent method
8723         OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
8725         this.updateItemVisibility();
8727         return this;
8731  * @inheritdoc
8732  */
8733 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
8734         // Parent method
8735         OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
8737         this.updateItemVisibility();
8739         return this;
8743  * Toggle visibility of the menu for screen readers.
8745  * @param {boolean} [screenReaderMode=false]
8746  */
8747 OO.ui.MenuSelectWidget.prototype.toggleScreenReaderMode = function ( screenReaderMode ) {
8748         screenReaderMode = !!screenReaderMode;
8749         this.screenReaderMode = screenReaderMode;
8751         this.$element.toggleClass( 'oo-ui-menuSelectWidget-screenReaderMode', this.screenReaderMode );
8753         if ( screenReaderMode ) {
8754                 this.bindDocumentKeyDownListener();
8755                 this.bindDocumentKeyPressListener();
8756         } else {
8757                 this.$focusOwner.removeAttr( 'aria-activedescendant' );
8758                 this.unbindDocumentKeyDownListener();
8759                 this.unbindDocumentKeyPressListener();
8760         }
8764  * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
8765  * `.toggle( true )` after its #$element is attached to the DOM.
8767  * Do not show the menu while it is not attached to the DOM. The calculations required to display
8768  * it in the right place and with the right dimensions only work correctly while it is attached.
8769  * Side-effects may include broken interface and exceptions being thrown. This wasn't always
8770  * strictly enforced, so currently it only generates a warning in the browser console.
8772  * @fires OO.ui.MenuSelectWidget#ready
8773  * @inheritdoc
8774  */
8775 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
8776         visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
8777         const change = visible !== this.isVisible();
8779         if ( visible && !this.warnedUnattached && !this.isElementAttached() ) {
8780                 OO.ui.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
8781                 this.warnedUnattached = true;
8782         }
8784         if ( change && visible ) {
8785                 // Reset position before showing the popup again. It's possible we no longer need to flip
8786                 // (e.g. if the user scrolled).
8787                 this.setVerticalPosition( this.originalVerticalPosition );
8788         }
8790         // Parent method
8791         OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
8793         if ( change ) {
8794                 if ( visible ) {
8796                         if ( this.width ) {
8797                                 this.setIdealSize( this.width );
8798                         } else if ( this.$floatableContainer ) {
8799                                 this.$clippable.css( 'width', 'auto' );
8800                                 this.setIdealSize(
8801                                         this.$floatableContainer[ 0 ].offsetWidth > this.$clippable[ 0 ].offsetWidth ?
8802                                                 // Dropdown is smaller than handle so expand to width
8803                                                 this.$floatableContainer[ 0 ].offsetWidth :
8804                                                 // Dropdown is larger than handle so auto size
8805                                                 'auto'
8806                                 );
8807                                 this.$clippable.css( 'width', '' );
8808                         }
8810                         this.togglePositioning( !!this.$floatableContainer );
8811                         this.toggleClipping( true );
8813                         if ( !this.screenReaderMode ) {
8814                                 this.bindDocumentKeyDownListener();
8815                                 this.bindDocumentKeyPressListener();
8816                         }
8818                         if (
8819                                 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
8820                                 this.originalVerticalPosition !== 'center'
8821                         ) {
8822                                 // If opening the menu in one direction causes it to be clipped, flip it
8823                                 const originalHeight = this.$element.height();
8824                                 this.setVerticalPosition(
8825                                         this.constructor.static.flippedPositions[ this.originalVerticalPosition ]
8826                                 );
8827                                 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
8828                                         // If flipping also causes it to be clipped, open in whichever direction
8829                                         // we have more space
8830                                         const flippedHeight = this.$element.height();
8831                                         if ( originalHeight > flippedHeight ) {
8832                                                 this.setVerticalPosition( this.originalVerticalPosition );
8833                                         }
8834                                 }
8835                         }
8836                         // Note that we do not flip the menu's opening direction if the clipping changes
8837                         // later (e.g. after the user scrolls), that seems like it would be annoying
8839                         this.$focusOwner.attr( 'aria-expanded', 'true' );
8840                         this.$focusOwner.attr( 'aria-owns', this.getElementId() );
8842                         const selectedItem = !this.multiselect && this.findSelectedItem();
8843                         if ( selectedItem ) {
8844                                 // TODO: Verify if this is even needed; This is already done on highlight changes
8845                                 // in SelectWidget#highlightItem, so we should just need to highlight the item
8846                                 // we need to highlight here and not bother with attr or checking selections.
8847                                 this.$focusOwner.attr( 'aria-activedescendant', selectedItem.getElementId() );
8848                                 selectedItem.scrollElementIntoView( { duration: 0 } );
8849                         }
8851                         // Auto-hide
8852                         if ( this.autoHide ) {
8853                                 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
8854                         }
8856                         this.emit( 'ready' );
8857                 } else {
8858                         this.$focusOwner.removeAttr( 'aria-activedescendant' );
8859                         if ( !this.screenReaderMode ) {
8860                                 this.unbindDocumentKeyDownListener();
8861                                 this.unbindDocumentKeyPressListener();
8862                         }
8863                         this.$focusOwner.attr( 'aria-expanded', 'false' );
8864                         this.$focusOwner.removeAttr( 'aria-owns' );
8865                         this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
8866                         this.togglePositioning( false );
8867                         this.toggleClipping( false );
8868                         this.lastHighlightedItem = null;
8869                 }
8870         }
8872         return this;
8876  * Scroll to the top of the menu
8877  */
8878 OO.ui.MenuSelectWidget.prototype.scrollToTop = function () {
8879         this.$element.scrollTop( 0 );
8883  * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
8884  * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
8885  * users can interact with it.
8887  * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8888  * OO.ui.DropdownInputWidget instead.
8890  * For more information, please see the [OOUI documentation on MediaWiki][1].
8892  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8894  *     @example
8895  *     // A DropdownWidget with a menu that contains three options.
8896  *     const dropdown = new OO.ui.DropdownWidget( {
8897  *         label: 'Dropdown menu: Select a menu option',
8898  *         menu: {
8899  *             items: [
8900  *                 new OO.ui.MenuOptionWidget( {
8901  *                     data: 'a',
8902  *                     label: 'First'
8903  *                 } ),
8904  *                 new OO.ui.MenuOptionWidget( {
8905  *                     data: 'b',
8906  *                     label: 'Second'
8907  *                 } ),
8908  *                 new OO.ui.MenuOptionWidget( {
8909  *                     data: 'c',
8910  *                     label: 'Third'
8911  *                 } )
8912  *             ]
8913  *         }
8914  *     } );
8916  *     $( document.body ).append( dropdown.$element );
8918  *     dropdown.getMenu().selectItemByData( 'b' );
8920  *     dropdown.getMenu().findSelectedItem().getData(); // Returns 'b'.
8922  * @class
8923  * @extends OO.ui.Widget
8924  * @mixes OO.ui.mixin.IconElement
8925  * @mixes OO.ui.mixin.IndicatorElement
8926  * @mixes OO.ui.mixin.LabelElement
8927  * @mixes OO.ui.mixin.TitledElement
8928  * @mixes OO.ui.mixin.TabIndexedElement
8930  * @constructor
8931  * @param {Object} [config] Configuration options
8932  * @param {Object} [config.menu] Configuration options to pass to
8933  *  {@link OO.ui.MenuSelectWidget menu select widget}.
8934  * @param {jQuery|boolean} [config.$overlay] Render the menu into a separate layer. This configuration is
8935  *  useful in cases where the expanded menu is larger than its containing `<div>`. The specified
8936  *  overlay layer is usually on top of the containing `<div>` and has a larger area. By default,
8937  *  the menu uses relative positioning. Pass 'true' to use the default overlay.
8938  *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
8939  */
8940 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
8941         // Configuration initialization
8942         config = Object.assign( { indicator: 'down' }, config );
8944         // Parent constructor
8945         OO.ui.DropdownWidget.super.call( this, config );
8947         // Properties (must be set before TabIndexedElement constructor call)
8948         this.$handle = $( '<span>' );
8949         this.$overlay = ( config.$overlay === true ?
8950                 OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
8952         // Mixin constructors
8953         OO.ui.mixin.IconElement.call( this, config );
8954         OO.ui.mixin.IndicatorElement.call( this, config );
8955         OO.ui.mixin.LabelElement.call( this, config );
8956         OO.ui.mixin.TitledElement.call( this, Object.assign( {
8957                 $titled: this.$label
8958         }, config ) );
8959         OO.ui.mixin.TabIndexedElement.call( this, Object.assign( {
8960                 $tabIndexed: this.$handle
8961         }, config ) );
8963         // Properties
8964         this.menu = new OO.ui.MenuSelectWidget( Object.assign( {
8965                 widget: this,
8966                 $floatableContainer: this.$element
8967         }, config.menu ) );
8969         // Events
8970         this.$handle.on( {
8971                 click: this.onClick.bind( this ),
8972                 keydown: this.onKeyDown.bind( this ),
8973                 focus: this.onFocus.bind( this ),
8974                 blur: this.onBlur.bind( this )
8975         } );
8976         this.menu.connect( this, {
8977                 select: 'onMenuSelect',
8978                 toggle: 'onMenuToggle'
8979         } );
8981         // Initialization
8982         const labelId = OO.ui.generateElementId();
8983         this.setLabelId( labelId );
8984         this.$label
8985                 .attr( {
8986                         role: 'textbox',
8987                         'aria-readonly': 'true'
8988                 } );
8989         this.$handle
8990                 .addClass( 'oo-ui-dropdownWidget-handle' )
8991                 .append( this.$icon, this.$label, this.$indicator )
8992                 .attr( {
8993                         role: 'combobox',
8994                         'aria-autocomplete': 'list',
8995                         'aria-expanded': 'false',
8996                         'aria-haspopup': 'true',
8997                         'aria-labelledby': labelId
8998                 } );
8999         this.$element
9000                 .addClass( 'oo-ui-dropdownWidget' )
9001                 .append( this.$handle );
9002         this.$overlay.append( this.menu.$element );
9005 /* Setup */
9007 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
9008 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
9009 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
9010 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
9011 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
9012 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
9014 /* Methods */
9017  * Get the menu.
9019  * @return {OO.ui.MenuSelectWidget} Menu of widget
9020  */
9021 OO.ui.DropdownWidget.prototype.getMenu = function () {
9022         return this.menu;
9026  * Handles menu select events.
9028  * @private
9029  * @param {OO.ui.MenuOptionWidget} item Selected menu item
9030  */
9031 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
9032         let selectedLabel;
9034         if ( !item ) {
9035                 this.setLabel( null );
9036                 return;
9037         }
9039         selectedLabel = item.getLabel();
9041         // If the label is a DOM element, clone it, because setLabel will append() it
9042         if ( selectedLabel instanceof $ ) {
9043                 selectedLabel = selectedLabel.clone();
9044         }
9046         this.setLabel( selectedLabel );
9050  * Handle menu toggle events.
9052  * @private
9053  * @param {boolean} isVisible Open state of the menu
9054  */
9055 OO.ui.DropdownWidget.prototype.onMenuToggle = function ( isVisible ) {
9056         this.$element.toggleClass( 'oo-ui-dropdownWidget-open', isVisible );
9060  * Handle mouse click events.
9062  * @private
9063  * @param {jQuery.Event} e Mouse click event
9064  * @return {undefined|boolean} False to prevent default if event is handled
9065  */
9066 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
9067         if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
9068                 this.menu.toggle();
9069         }
9070         return false;
9074  * Handle key down events.
9076  * @private
9077  * @param {jQuery.Event} e Key down event
9078  * @return {undefined|boolean} False to prevent default if event is handled
9079  */
9080 OO.ui.DropdownWidget.prototype.onKeyDown = function ( e ) {
9081         if ( !this.isDisabled() ) {
9082                 switch ( e.keyCode ) {
9083                         case OO.ui.Keys.ENTER:
9084                                 this.menu.toggle();
9085                                 return false;
9086                         case OO.ui.Keys.SPACE:
9087                                 if ( this.menu.keyPressBuffer === '' ) {
9088                                         // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
9089                                         // Space only opens the menu is the user is not typing to search.
9090                                         this.menu.toggle();
9091                                         return false;
9092                                 }
9093                                 break;
9094                 }
9095         }
9099  * Handle focus events.
9101  * @private
9102  * @param {jQuery.Event} e Focus event
9103  */
9104 OO.ui.DropdownWidget.prototype.onFocus = function () {
9105         this.menu.toggleScreenReaderMode( true );
9109  * Handle blur events.
9111  * @private
9112  * @param {jQuery.Event} e Blur event
9113  */
9114 OO.ui.DropdownWidget.prototype.onBlur = function () {
9115         this.menu.toggleScreenReaderMode( false );
9119  * @inheritdoc
9120  */
9121 OO.ui.DropdownWidget.prototype.setLabelledBy = function ( id ) {
9122         const labelId = this.$label.attr( 'id' );
9124         if ( id ) {
9125                 this.$handle.attr( 'aria-labelledby', id + ' ' + labelId );
9126         } else {
9127                 this.$handle.attr( 'aria-labelledby', labelId );
9128         }
9132  * RadioOptionWidget is an option widget that looks like a radio button.
9133  * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
9134  * Please see the [OOUI documentation on MediaWiki][1] for more information.
9136  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
9138  * @class
9139  * @extends OO.ui.OptionWidget
9141  * @constructor
9142  * @param {Object} [config] Configuration options
9143  * @param {any} [config.data]
9144  */
9145 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
9146         // Configuration initialization
9147         config = config || {};
9149         // Properties (must be done before parent constructor which calls #setDisabled)
9150         this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
9152         // Parent constructor
9153         OO.ui.RadioOptionWidget.super.call( this, config );
9155         // Initialization
9156         // Remove implicit role, we're handling it ourselves
9157         this.radio.$input.attr( 'role', 'presentation' );
9158         this.$element
9159                 .addClass( 'oo-ui-radioOptionWidget' )
9160                 .attr( { role: 'radio', 'aria-checked': 'false' } )
9161                 .removeAttr( 'aria-selected' )
9162                 .prepend( this.radio.$element );
9165 /* Setup */
9167 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
9169 /* Static Properties */
9172  * @static
9173  * @inheritdoc
9174  */
9175 OO.ui.RadioOptionWidget.static.highlightable = false;
9178  * @static
9179  * @inheritdoc
9180  */
9181 OO.ui.RadioOptionWidget.static.pressable = false;
9184  * @static
9185  * @inheritdoc
9186  */
9187 OO.ui.RadioOptionWidget.static.tagName = 'label';
9189 /* Methods */
9192  * @inheritdoc
9193  */
9194 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
9195         OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
9197         this.radio.setSelected( state );
9198         this.$element
9199                 .attr( 'aria-checked', this.selected.toString() )
9200                 .removeAttr( 'aria-selected' );
9202         return this;
9206  * @inheritdoc
9207  */
9208 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
9209         OO.ui.RadioOptionWidget.super.prototype.setDisabled.call( this, disabled );
9211         this.radio.setDisabled( this.isDisabled() );
9213         return this;
9217  * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
9218  * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
9219  * an interface for adding, removing and selecting options.
9220  * Please see the [OOUI documentation on MediaWiki][1] for more information.
9222  * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
9223  * OO.ui.RadioSelectInputWidget instead.
9225  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
9227  *     @example
9228  *     // A RadioSelectWidget with RadioOptions.
9229  *     const option1 = new OO.ui.RadioOptionWidget( {
9230  *             data: 'a',
9231  *             label: 'Selected radio option'
9232  *         } ),
9233  *         option2 = new OO.ui.RadioOptionWidget( {
9234  *             data: 'b',
9235  *             label: 'Unselected radio option'
9236  *         } );
9237  *         radioSelect = new OO.ui.RadioSelectWidget( {
9238  *             items: [ option1, option2 ]
9239  *         } );
9241  *     // Select 'option 1' using the RadioSelectWidget's selectItem() method.
9242  *     radioSelect.selectItem( option1 );
9244  *     $( document.body ).append( radioSelect.$element );
9246  * @class
9247  * @extends OO.ui.SelectWidget
9248  * @mixes OO.ui.mixin.TabIndexedElement
9250  * @constructor
9251  * @param {Object} [config] Configuration options
9252  */
9253 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
9254         // Parent constructor
9255         OO.ui.RadioSelectWidget.super.call( this, config );
9257         // Mixin constructors
9258         OO.ui.mixin.TabIndexedElement.call( this, config );
9260         // Events
9261         this.$element.on( {
9262                 focus: this.bindDocumentKeyDownListener.bind( this ),
9263                 blur: this.unbindDocumentKeyDownListener.bind( this )
9264         } );
9266         // Initialization
9267         this.$element
9268                 .addClass( 'oo-ui-radioSelectWidget' )
9269                 .attr( 'role', 'radiogroup' )
9270                 // Not applicable to 'radiogroup', and it would always be 'false' anyway
9271                 .removeAttr( 'aria-multiselectable' );
9274 /* Setup */
9276 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
9277 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
9280  * MultioptionWidgets are special elements that can be selected and configured with data. The
9281  * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
9282  * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
9283  * and examples, please see the [OOUI documentation on MediaWiki][1].
9285  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
9287  * @class
9288  * @extends OO.ui.Widget
9289  * @mixes OO.ui.mixin.ItemWidget
9290  * @mixes OO.ui.mixin.LabelElement
9291  * @mixes OO.ui.mixin.TitledElement
9293  * @constructor
9294  * @param {Object} [config] Configuration options
9295  * @param {boolean} [config.selected=false] Whether the option is initially selected
9296  */
9297 OO.ui.MultioptionWidget = function OoUiMultioptionWidget( config ) {
9298         // Configuration initialization
9299         config = config || {};
9301         // Parent constructor
9302         OO.ui.MultioptionWidget.super.call( this, config );
9304         // Mixin constructors
9305         OO.ui.mixin.ItemWidget.call( this );
9306         OO.ui.mixin.LabelElement.call( this, config );
9307         OO.ui.mixin.TitledElement.call( this, config );
9309         // Properties
9310         this.selected = null;
9312         // Initialization
9313         this.$element
9314                 .addClass( 'oo-ui-multioptionWidget' )
9315                 .append( this.$label );
9316         this.setSelected( config.selected );
9319 /* Setup */
9321 OO.inheritClass( OO.ui.MultioptionWidget, OO.ui.Widget );
9322 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.ItemWidget );
9323 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.LabelElement );
9324 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.TitledElement );
9326 /* Events */
9329  * A change event is emitted when the selected state of the option changes.
9331  * @event OO.ui.MultioptionWidget#change
9332  * @param {boolean} selected Whether the option is now selected
9333  */
9335 /* Methods */
9338  * Check if the option is selected.
9340  * @return {boolean} Item is selected
9341  */
9342 OO.ui.MultioptionWidget.prototype.isSelected = function () {
9343         return this.selected;
9347  * Set the option’s selected state. In general, all modifications to the selection
9348  * should be handled by the SelectWidget’s
9349  * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
9351  * @param {boolean} [state=false] Select option
9352  * @chainable
9353  * @return {OO.ui.Widget} The widget, for chaining
9354  */
9355 OO.ui.MultioptionWidget.prototype.setSelected = function ( state ) {
9356         state = !!state;
9357         if ( this.selected !== state ) {
9358                 this.selected = state;
9359                 this.emit( 'change', state );
9360                 this.$element.toggleClass( 'oo-ui-multioptionWidget-selected', state );
9361         }
9362         return this;
9366  * MultiselectWidget allows selecting multiple options from a list.
9368  * For more information about menus and options, please see the [OOUI documentation
9369  * on MediaWiki][1].
9371  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
9373  * @class
9374  * @abstract
9375  * @extends OO.ui.Widget
9376  * @mixes OO.ui.mixin.GroupWidget
9377  * @mixes OO.ui.mixin.TitledElement
9379  * @constructor
9380  * @param {Object} [config] Configuration options
9381  * @param {OO.ui.MultioptionWidget[]} [config.items] An array of options to add to the multiselect.
9382  */
9383 OO.ui.MultiselectWidget = function OoUiMultiselectWidget( config ) {
9384         // Parent constructor
9385         OO.ui.MultiselectWidget.super.call( this, config );
9387         // Configuration initialization
9388         config = config || {};
9390         // Mixin constructors
9391         OO.ui.mixin.GroupWidget.call( this, config );
9392         OO.ui.mixin.TitledElement.call( this, config );
9394         // Events
9395         this.aggregate( {
9396                 change: 'select'
9397         } );
9398         // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
9399         // by GroupElement only when items are added/removed
9400         this.connect( this, {
9401                 select: [ 'emit', 'change' ]
9402         } );
9404         // Initialization
9405         this.addItems( config.items || [] );
9406         this.$group.addClass( 'oo-ui-multiselectWidget-group' );
9407         this.$element.addClass( 'oo-ui-multiselectWidget' )
9408                 .append( this.$group );
9411 /* Setup */
9413 OO.inheritClass( OO.ui.MultiselectWidget, OO.ui.Widget );
9414 OO.mixinClass( OO.ui.MultiselectWidget, OO.ui.mixin.GroupWidget );
9415 OO.mixinClass( OO.ui.MultiselectWidget, OO.ui.mixin.TitledElement );
9417 /* Events */
9420  * A change event is emitted when the set of items changes, or an item is selected or deselected.
9422  * @event OO.ui.MultiselectWidget#change
9423  */
9426  * A select event is emitted when an item is selected or deselected.
9428  * @event OO.ui.MultiselectWidget#select
9429  */
9431 /* Methods */
9434  * Find options that are selected.
9436  * @return {OO.ui.MultioptionWidget[]} Selected options
9437  */
9438 OO.ui.MultiselectWidget.prototype.findSelectedItems = function () {
9439         return this.items.filter( ( item ) => item.isSelected() );
9443  * Find the data of options that are selected.
9445  * @return {Object[]|string[]} Values of selected options
9446  */
9447 OO.ui.MultiselectWidget.prototype.findSelectedItemsData = function () {
9448         return this.findSelectedItems().map( ( item ) => item.data );
9452  * Select options by reference. Options not mentioned in the `items` array will be deselected.
9454  * @param {OO.ui.MultioptionWidget[]} items Items to select
9455  * @chainable
9456  * @return {OO.ui.Widget} The widget, for chaining
9457  */
9458 OO.ui.MultiselectWidget.prototype.selectItems = function ( items ) {
9459         const itemsSet = new Set( items );
9460         this.items.forEach( ( item ) => {
9461                 const selected = itemsSet.has( item );
9462                 item.setSelected( selected );
9463         } );
9464         return this;
9468  * Select items by their data. Options not mentioned in the `datas` array will be deselected.
9470  * @param {Object[]|string[]} datas Values of items to select
9471  * @chainable
9472  * @return {OO.ui.Widget} The widget, for chaining
9473  */
9474 OO.ui.MultiselectWidget.prototype.selectItemsByData = function ( datas ) {
9475         const dataHashSet = new Set( datas.map( ( data ) => OO.getHash( data ) ) );
9476         this.items.forEach( ( item ) => {
9477                 const selected = dataHashSet.has( OO.getHash( item.getData() ) );
9478                 item.setSelected( selected );
9479         } );
9480         return this;
9484  * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
9485  * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
9486  * Please see the [OOUI documentation on MediaWiki][1] for more information.
9488  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
9490  * @class
9491  * @extends OO.ui.MultioptionWidget
9493  * @constructor
9494  * @param {Object} [config] Configuration options
9495  */
9496 OO.ui.CheckboxMultioptionWidget = function OoUiCheckboxMultioptionWidget( config ) {
9497         // Configuration initialization
9498         config = config || {};
9500         // Properties (must be done before parent constructor which calls #setDisabled)
9501         this.checkbox = new OO.ui.CheckboxInputWidget();
9503         // Parent constructor
9504         OO.ui.CheckboxMultioptionWidget.super.call( this, config );
9506         // Events
9507         this.checkbox.on( 'change', this.onCheckboxChange.bind( this ) );
9508         this.$element.on( 'keydown', this.onKeyDown.bind( this ) );
9510         // Initialization
9511         this.$element
9512                 .addClass( 'oo-ui-checkboxMultioptionWidget' )
9513                 .prepend( this.checkbox.$element );
9516 /* Setup */
9518 OO.inheritClass( OO.ui.CheckboxMultioptionWidget, OO.ui.MultioptionWidget );
9520 /* Static Properties */
9523  * @static
9524  * @inheritdoc
9525  */
9526 OO.ui.CheckboxMultioptionWidget.static.tagName = 'label';
9528 /* Methods */
9531  * Handle checkbox selected state change.
9533  * @private
9534  */
9535 OO.ui.CheckboxMultioptionWidget.prototype.onCheckboxChange = function () {
9536         this.setSelected( this.checkbox.isSelected() );
9540  * @inheritdoc
9541  */
9542 OO.ui.CheckboxMultioptionWidget.prototype.setSelected = function ( state ) {
9543         OO.ui.CheckboxMultioptionWidget.super.prototype.setSelected.call( this, state );
9544         this.checkbox.setSelected( state );
9545         return this;
9549  * @inheritdoc
9550  */
9551 OO.ui.CheckboxMultioptionWidget.prototype.setDisabled = function ( disabled ) {
9552         OO.ui.CheckboxMultioptionWidget.super.prototype.setDisabled.call( this, disabled );
9553         this.checkbox.setDisabled( this.isDisabled() );
9554         return this;
9558  * Focus the widget.
9559  */
9560 OO.ui.CheckboxMultioptionWidget.prototype.focus = function () {
9561         this.checkbox.focus();
9565  * Handle key down events.
9567  * @protected
9568  * @param {jQuery.Event} e
9569  */
9570 OO.ui.CheckboxMultioptionWidget.prototype.onKeyDown = function ( e ) {
9571         const element = this.getElementGroup();
9573         let nextItem;
9574         if ( e.keyCode === OO.ui.Keys.LEFT || e.keyCode === OO.ui.Keys.UP ) {
9575                 nextItem = element.getRelativeFocusableItem( this, -1 );
9576         } else if ( e.keyCode === OO.ui.Keys.RIGHT || e.keyCode === OO.ui.Keys.DOWN ) {
9577                 nextItem = element.getRelativeFocusableItem( this, 1 );
9578         }
9580         if ( nextItem ) {
9581                 e.preventDefault();
9582                 nextItem.focus();
9583         }
9587  * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
9588  * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
9589  * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
9590  * Please see the [OOUI documentation on MediaWiki][1] for more information.
9592  * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
9593  * OO.ui.CheckboxMultiselectInputWidget instead.
9595  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
9597  *     @example
9598  *     // A CheckboxMultiselectWidget with CheckboxMultioptions.
9599  *     const option1 = new OO.ui.CheckboxMultioptionWidget( {
9600  *             data: 'a',
9601  *             selected: true,
9602  *             label: 'Selected checkbox'
9603  *         } ),
9604  *         option2 = new OO.ui.CheckboxMultioptionWidget( {
9605  *             data: 'b',
9606  *             label: 'Unselected checkbox'
9607  *         } ),
9608  *         multiselect = new OO.ui.CheckboxMultiselectWidget( {
9609  *             items: [ option1, option2 ]
9610  *         } );
9611  *     $( document.body ).append( multiselect.$element );
9613  * @class
9614  * @extends OO.ui.MultiselectWidget
9616  * @constructor
9617  * @param {Object} [config] Configuration options
9618  */
9619 OO.ui.CheckboxMultiselectWidget = function OoUiCheckboxMultiselectWidget( config ) {
9620         // Parent constructor
9621         OO.ui.CheckboxMultiselectWidget.super.call( this, config );
9623         // Properties
9624         this.$lastClicked = null;
9626         // Events
9627         this.$group.on( 'click', this.onClick.bind( this ) );
9629         // Initialization
9630         this.$element.addClass( 'oo-ui-checkboxMultiselectWidget' );
9633 /* Setup */
9635 OO.inheritClass( OO.ui.CheckboxMultiselectWidget, OO.ui.MultiselectWidget );
9637 /* Methods */
9640  * Get an option by its position relative to the specified item (or to the start of the
9641  * option array, if item is `null`). The direction in which to search through the option array
9642  * is specified with a number: -1 for reverse (the default) or 1 for forward. The method will
9643  * return an option, or `null` if there are no options in the array.
9645  * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or
9646  *  `null` to start at the beginning of the array.
9647  * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
9648  * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items
9649  *  in the select.
9650  */
9651 OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function ( item, direction ) {
9652         const increase = direction > 0 ? 1 : -1,
9653                 len = this.items.length;
9655         let nextIndex;
9656         if ( item ) {
9657                 const currentIndex = this.items.indexOf( item );
9658                 nextIndex = ( currentIndex + increase + len ) % len;
9659         } else {
9660                 // If no item is selected and moving forward, start at the beginning.
9661                 // If moving backward, start at the end.
9662                 nextIndex = direction > 0 ? 0 : len - 1;
9663         }
9665         for ( let i = 0; i < len; i++ ) {
9666                 item = this.items[ nextIndex ];
9667                 if ( item && !item.isDisabled() ) {
9668                         return item;
9669                 }
9670                 nextIndex = ( nextIndex + increase + len ) % len;
9671         }
9672         return null;
9676  * Handle click events on checkboxes.
9678  * @param {jQuery.Event} e
9679  */
9680 OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
9681         const $lastClicked = this.$lastClicked,
9682                 $nowClicked = $( e.target ).closest( '.oo-ui-checkboxMultioptionWidget' )
9683                         .not( '.oo-ui-widget-disabled' );
9685         // Allow selecting multiple options at once by Shift-clicking them
9686         if ( $lastClicked && $nowClicked.length && e.shiftKey ) {
9687                 const $options = this.$group.find( '.oo-ui-checkboxMultioptionWidget' );
9688                 const lastClickedIndex = $options.index( $lastClicked );
9689                 const nowClickedIndex = $options.index( $nowClicked );
9690                 // If it's the same item, either the user is being silly, or it's a fake event generated
9691                 // by the browser. In either case we don't need custom handling.
9692                 if ( nowClickedIndex !== lastClickedIndex ) {
9693                         const items = this.items;
9694                         const wasSelected = items[ nowClickedIndex ].isSelected();
9695                         const direction = nowClickedIndex > lastClickedIndex ? 1 : -1;
9697                         // This depends on the DOM order of the items and the order of the .items array being
9698                         // the same.
9699                         for ( let i = lastClickedIndex; i !== nowClickedIndex; i += direction ) {
9700                                 if ( !items[ i ].isDisabled() ) {
9701                                         items[ i ].setSelected( !wasSelected );
9702                                 }
9703                         }
9704                         // For the now-clicked element, use immediate timeout to allow the browser to do its own
9705                         // handling first, then set our value. The order in which events happen is different for
9706                         // clicks on the <input> and on the <label> and there are additional fake clicks fired
9707                         // for non-click actions that change the checkboxes.
9708                         e.preventDefault();
9709                         setTimeout( () => {
9710                                 if ( !items[ nowClickedIndex ].isDisabled() ) {
9711                                         items[ nowClickedIndex ].setSelected( !wasSelected );
9712                                 }
9713                         } );
9714                 }
9715         }
9717         if ( $nowClicked.length ) {
9718                 this.$lastClicked = $nowClicked;
9719         }
9723  * Focus the widget
9725  * @chainable
9726  * @return {OO.ui.Widget} The widget, for chaining
9727  */
9728 OO.ui.CheckboxMultiselectWidget.prototype.focus = function () {
9729         if ( !this.isDisabled() ) {
9730                 const item = this.getRelativeFocusableItem( null, 1 );
9731                 if ( item ) {
9732                         item.focus();
9733                 }
9734         }
9735         return this;
9739  * @inheritdoc
9740  */
9741 OO.ui.CheckboxMultiselectWidget.prototype.simulateLabelClick = function () {
9742         this.focus();
9746  * Progress bars visually display the status of an operation, such as a download,
9747  * and can be either determinate or indeterminate:
9749  * - **determinate** process bars show the percent of an operation that is complete.
9751  * - **indeterminate** process bars use a visual display of motion to indicate that an operation
9752  *   is taking place. Because the extent of an indeterminate operation is unknown, the bar does
9753  *   not use percentages.
9755  * The value of the `progress` configuration determines whether the bar is determinate
9756  * or indeterminate.
9758  *     @example
9759  *     // Examples of determinate and indeterminate progress bars.
9760  *     const progressBar1 = new OO.ui.ProgressBarWidget( {
9761  *         progress: 33
9762  *     } );
9763  *     const progressBar2 = new OO.ui.ProgressBarWidget();
9765  *     // Create a FieldsetLayout to layout progress bars.
9766  *     const fieldset = new OO.ui.FieldsetLayout;
9767  *     fieldset.addItems( [
9768  *         new OO.ui.FieldLayout( progressBar1, {
9769  *             label: 'Determinate',
9770  *             align: 'top'
9771  *         } ),
9772  *         new OO.ui.FieldLayout( progressBar2, {
9773  *             label: 'Indeterminate',
9774  *             align: 'top'
9775  *         } )
9776  *     ] );
9777  *     $( document.body ).append( fieldset.$element );
9779  * @class
9780  * @extends OO.ui.Widget
9781  * @mixes OO.ui.mixin.PendingElement
9783  * @constructor
9784  * @param {Object} [config] Configuration options
9785  * @param {number|boolean} [config.progress=false] The type of progress bar (determinate or indeterminate).
9786  *  To create a determinate progress bar, specify a number that reflects the initial
9787  *  percent complete.
9788  *  By default, the progress bar is indeterminate.
9789  * @param {boolean} [config.inline=false] Use a smaller inline variant on the progress bar
9790  */
9791 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
9792         // Configuration initialization
9793         config = config || {};
9795         // Parent constructor
9796         OO.ui.ProgressBarWidget.super.call( this, config );
9798         // Mixin constructors
9799         OO.ui.mixin.PendingElement.call( this, config );
9801         // Properties
9802         this.$bar = $( '<div>' );
9803         this.progress = null;
9805         // Initialization
9806         this.setProgress( config.progress !== undefined ? config.progress : false );
9807         this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
9808         this.$element
9809                 .attr( {
9810                         role: 'progressbar',
9811                         'aria-valuemin': 0,
9812                         'aria-valuemax': 100
9813                 } )
9814                 .addClass( 'oo-ui-progressBarWidget' )
9815                 .append( this.$bar );
9817         if ( config.inline ) {
9818                 this.$element.addClass( 'oo-ui-progressBarWidget-inline' );
9819         }
9822 /* Setup */
9824 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
9825 OO.mixinClass( OO.ui.ProgressBarWidget, OO.ui.mixin.PendingElement );
9827 /* Static Properties */
9830  * @static
9831  * @inheritdoc
9832  */
9833 OO.ui.ProgressBarWidget.static.tagName = 'div';
9835 /* Methods */
9838  * Get the percent of the progress that has been completed. Indeterminate progresses will
9839  * return `false`.
9841  * @return {number|boolean} Progress percent
9842  */
9843 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
9844         return this.progress;
9848  * Set the percent of the process completed or `false` for an indeterminate process.
9850  * @param {number|boolean} progress Progress percent or `false` for indeterminate
9851  */
9852 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
9853         this.progress = progress;
9855         if ( progress !== false ) {
9856                 this.$bar.css( 'width', this.progress + '%' );
9857                 this.$element.attr( 'aria-valuenow', this.progress );
9858         } else {
9859                 this.$bar.css( 'width', '' );
9860                 this.$element.removeAttr( 'aria-valuenow' );
9861         }
9862         this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress === false );
9866  * InputWidget is the base class for all input widgets, which
9867  * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox
9868  * inputs}, {@link OO.ui.RadioInputWidget radio inputs}, and
9869  * {@link OO.ui.ButtonInputWidget button inputs}.
9870  * See the [OOUI documentation on MediaWiki][1] for more information and examples.
9872  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9874  * @abstract
9875  * @class
9876  * @extends OO.ui.Widget
9877  * @mixes OO.ui.mixin.TabIndexedElement
9878  * @mixes OO.ui.mixin.TitledElement
9879  * @mixes OO.ui.mixin.AccessKeyedElement
9881  * @constructor
9882  * @param {Object} [config] Configuration options
9883  * @param {string} [config.name=''] The value of the input’s HTML `name` attribute.
9884  * @param {string} [config.value=''] The value of the input.
9885  * @param {string} [config.dir] The directionality of the input (ltr/rtl).
9886  * @param {string} [config.inputId] The value of the input’s HTML `id` attribute.
9887  * @param {Function} [config.inputFilter] The name of an input filter function. Input filters modify the
9888  *  value of an input before it is accepted.
9889  */
9890 OO.ui.InputWidget = function OoUiInputWidget( config ) {
9891         // Configuration initialization
9892         config = config || {};
9894         // Parent constructor
9895         OO.ui.InputWidget.super.call( this, config );
9897         // Properties
9898         // See #reusePreInfuseDOM about config.$input
9899         this.$input = config.$input || this.getInputElement( config );
9900         this.value = '';
9901         this.inputFilter = config.inputFilter;
9903         // Mixin constructors
9904         OO.ui.mixin.TabIndexedElement.call( this, Object.assign( {
9905                 $tabIndexed: this.$input
9906         }, config ) );
9907         OO.ui.mixin.TitledElement.call( this, Object.assign( {
9908                 $titled: this.$input
9909         }, config ) );
9910         OO.ui.mixin.AccessKeyedElement.call( this, Object.assign( {
9911                 $accessKeyed: this.$input
9912         }, config ) );
9914         // Events
9915         this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
9917         // Initialization
9918         this.$input
9919                 .addClass( 'oo-ui-inputWidget-input' )
9920                 .attr( 'name', config.name )
9921                 .prop( 'disabled', this.isDisabled() );
9922         this.$element
9923                 .addClass( 'oo-ui-inputWidget' )
9924                 .append( this.$input );
9925         this.setValue( config.value );
9926         if ( config.dir ) {
9927                 this.setDir( config.dir );
9928         }
9929         if ( config.inputId !== undefined ) {
9930                 this.setInputId( config.inputId );
9931         }
9934 /* Setup */
9936 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
9937 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
9938 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
9939 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
9941 /* Static Methods */
9944  * @inheritdoc
9945  */
9946 OO.ui.InputWidget.static.reusePreInfuseDOM = function ( node, config ) {
9947         const $input = $( node ).find( '.oo-ui-inputWidget-input' );
9948         config = OO.ui.InputWidget.super.static.reusePreInfuseDOM( node, config );
9949         // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
9950         if ( $input.length ) {
9951                 config.$input = $input;
9952         }
9953         return config;
9957  * @inheritdoc
9958  */
9959 OO.ui.InputWidget.static.gatherPreInfuseState = function ( node, config ) {
9960         const state = OO.ui.InputWidget.super.static.gatherPreInfuseState( node, config );
9961         if ( config.$input ) {
9962                 state.value = config.$input.val();
9963                 // Might be better in TabIndexedElement, but it's awkward to do there because
9964                 // mixins are awkward
9965                 state.focus = config.$input.is( ':focus' );
9966         }
9967         return state;
9970 /* Events */
9973  * A change event is emitted when the value of the input changes.
9975  * @event OO.ui.InputWidget#change
9976  * @param {string} value
9977  */
9979 /* Methods */
9982  * Get input element.
9984  * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9985  * different circumstances. The element must have a `value` property (like form elements).
9987  * @protected
9988  * @param {Object} config Configuration options
9989  * @return {jQuery} Input element
9990  */
9991 OO.ui.InputWidget.prototype.getInputElement = function () {
9992         return $( '<input>' );
9996  * Handle potentially value-changing events.
9998  * @private
9999  * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
10000  */
10001 OO.ui.InputWidget.prototype.onEdit = function () {
10002         if ( !this.isDisabled() ) {
10003                 this.setValue( this.$input.val() );
10004                 // Allow the stack to clear so the value will be updated
10005                 // TODO: This appears to only be used by TextInputWidget, and in current browsers
10006                 // they always the value immediately, however it is mostly harmless so this can be
10007                 // left in until more thoroughly tested.
10008                 setTimeout( () => {
10009                         this.setValue( this.$input.val() );
10010                 } );
10011         }
10015  * Get the value of the input.
10017  * @return {string} Input value
10018  */
10019 OO.ui.InputWidget.prototype.getValue = function () {
10020         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10021         // it, and we won't know unless they're kind enough to trigger a 'change' event.
10022         const value = this.$input.val();
10023         if ( this.value !== value ) {
10024                 this.setValue( value );
10025         }
10026         return this.value;
10030  * Set the directionality of the input.
10032  * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
10033  * @chainable
10034  * @return {OO.ui.Widget} The widget, for chaining
10035  */
10036 OO.ui.InputWidget.prototype.setDir = function ( dir ) {
10037         this.$input.prop( 'dir', dir );
10038         return this;
10042  * Set the value of the input.
10044  * @param {string} value New value
10045  * @fires OO.ui.InputWidget#change
10046  * @chainable
10047  * @return {OO.ui.Widget} The widget, for chaining
10048  */
10049 OO.ui.InputWidget.prototype.setValue = function ( value ) {
10050         value = this.cleanUpValue( value );
10051         // Update the DOM if it has changed. Note that with cleanUpValue, it
10052         // is possible for the DOM value to change without this.value changing.
10053         if ( this.$input.val() !== value ) {
10054                 this.$input.val( value );
10055         }
10056         if ( this.value !== value ) {
10057                 this.value = value;
10058                 this.emit( 'change', this.value );
10059         }
10060         // The first time that the value is set (probably while constructing the widget),
10061         // remember it in defaultValue. This property can be later used to check whether
10062         // the value of the input has been changed since it was created.
10063         if ( this.defaultValue === undefined ) {
10064                 this.defaultValue = this.value;
10065                 this.$input[ 0 ].defaultValue = this.defaultValue;
10066         }
10067         return this;
10071  * Clean up incoming value.
10073  * Ensures value is a string, and converts undefined and null to empty string.
10075  * @private
10076  * @param {string} value Original value
10077  * @return {string} Cleaned up value
10078  */
10079 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
10080         if ( value === undefined || value === null ) {
10081                 return '';
10082         } else if ( this.inputFilter ) {
10083                 return this.inputFilter( String( value ) );
10084         } else {
10085                 return String( value );
10086         }
10090  * @inheritdoc
10091  */
10092 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
10093         OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
10094         if ( this.$input ) {
10095                 this.$input.prop( 'disabled', this.isDisabled() );
10096         }
10097         return this;
10101  * Set the 'id' attribute of the `<input>` element.
10103  * @param {string} id
10104  * @chainable
10105  * @return {OO.ui.Widget} The widget, for chaining
10106  */
10107 OO.ui.InputWidget.prototype.setInputId = function ( id ) {
10108         this.$input.attr( 'id', id );
10109         return this;
10113  * @inheritdoc
10114  */
10115 OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
10116         OO.ui.InputWidget.super.prototype.restorePreInfuseState.call( this, state );
10117         if ( state.value !== undefined && state.value !== this.getValue() ) {
10118                 this.setValue( state.value );
10119         }
10120         if ( state.focus ) {
10121                 this.focus();
10122         }
10126  * Data widget intended for creating `<input type="hidden">` inputs.
10128  * @class
10129  * @extends OO.ui.Widget
10131  * @constructor
10132  * @param {Object} [config] Configuration options
10133  * @param {string} [config.value=''] The value of the input.
10134  * @param {string} [config.name=''] The value of the input’s HTML `name` attribute.
10135  */
10136 OO.ui.HiddenInputWidget = function OoUiHiddenInputWidget( config ) {
10137         // Configuration initialization
10138         config = Object.assign( { value: '', name: '' }, config );
10140         // Parent constructor
10141         OO.ui.HiddenInputWidget.super.call( this, config );
10143         // Initialization
10144         this.$element.attr( {
10145                 type: 'hidden',
10146                 value: config.value,
10147                 name: config.name
10148         } );
10149         this.$element.removeAttr( 'aria-disabled' );
10152 /* Setup */
10154 OO.inheritClass( OO.ui.HiddenInputWidget, OO.ui.Widget );
10156 /* Static Properties */
10159  * @static
10160  * @inheritdoc
10161  */
10162 OO.ui.HiddenInputWidget.static.tagName = 'input';
10165  * ButtonInputWidget is used to submit HTML forms and is intended to be used within
10166  * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
10167  * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
10168  * HTML `<button>` (the default) or an HTML `<input>` tags. See the
10169  * [OOUI documentation on MediaWiki][1] for more information.
10171  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
10173  *     @example
10174  *     // A ButtonInputWidget rendered as an HTML button, the default.
10175  *     const button = new OO.ui.ButtonInputWidget( {
10176  *         label: 'Input button',
10177  *         icon: 'check',
10178  *         value: 'check'
10179  *     } );
10180  *     $( document.body ).append( button.$element );
10182  * @class
10183  * @extends OO.ui.InputWidget
10184  * @mixes OO.ui.mixin.ButtonElement
10185  * @mixes OO.ui.mixin.IconElement
10186  * @mixes OO.ui.mixin.IndicatorElement
10187  * @mixes OO.ui.mixin.LabelElement
10188  * @mixes OO.ui.mixin.FlaggedElement
10190  * @constructor
10191  * @param {Object} [config] Configuration options
10192  * @param {string} [config.type='button'] The value of the HTML `'type'` attribute:
10193  *  'button', 'submit' or 'reset'.
10194  * @param {boolean} [config.useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the
10195  *  default. Widgets configured to be an `<input>` do not support {@link OO.ui.ButtonInputWidget#icon icons} and
10196  *  {@link OO.ui.ButtonInputWidget#indicator indicators}, non-plaintext {@link OO.ui.ButtonInputWidget#label labels}, or {@link OO.ui.ButtonInputWidget#value values}. In
10197  *  general, useInputTag should only be set to `true` when there’s need to support IE 6 in a form
10198  *  with multiple buttons.
10199  * @param {boolean} [config.formNoValidate=false] Whether to use `formnovalidate` attribute.
10200  */
10201 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
10202         // Configuration initialization
10203         config = Object.assign( { type: 'button', useInputTag: false, formNoValidate: false }, config );
10205         // See InputWidget#reusePreInfuseDOM about config.$input
10206         if ( config.$input ) {
10207                 config.$input.empty();
10208         }
10210         // Properties (must be set before parent constructor, which calls #setValue)
10211         this.useInputTag = config.useInputTag;
10213         // Parent constructor
10214         OO.ui.ButtonInputWidget.super.call( this, config );
10216         // Mixin constructors
10217         OO.ui.mixin.ButtonElement.call( this, Object.assign( {
10218                 $button: this.$input
10219         }, config ) );
10220         OO.ui.mixin.IconElement.call( this, config );
10221         OO.ui.mixin.IndicatorElement.call( this, config );
10222         OO.ui.mixin.LabelElement.call( this, config );
10223         OO.ui.mixin.FlaggedElement.call( this, config );
10225         // Initialization
10226         if ( !config.useInputTag ) {
10227                 this.$input.append( this.$icon, this.$label, this.$indicator );
10228         }
10230         if ( config.formNoValidate ) {
10231                 this.$input.attr( 'formnovalidate', 'formnovalidate' );
10232         }
10234         this.$element.addClass( 'oo-ui-buttonInputWidget' );
10237 /* Setup */
10239 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
10240 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
10241 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
10242 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
10243 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
10244 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.FlaggedElement );
10246 /* Static Properties */
10249  * @static
10250  * @inheritdoc
10251  */
10252 OO.ui.ButtonInputWidget.static.tagName = 'span';
10254 /* Methods */
10257  * @inheritdoc
10258  * @protected
10259  */
10260 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
10261         const type = config.type === 'submit' || config.type === 'reset' ? config.type : 'button';
10262         return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
10266  * Set label value.
10268  * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
10270  * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
10271  *  text, or `null` for no label
10272  * @chainable
10273  * @return {OO.ui.Widget} The widget, for chaining
10274  */
10275 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
10276         label = OO.ui.resolveMsg( label );
10278         if ( this.useInputTag ) {
10279                 // Discard non-plaintext labels
10280                 if ( typeof label !== 'string' ) {
10281                         label = '';
10282                 }
10284                 this.$input.val( label );
10285         }
10287         return OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
10291  * Set the value of the input.
10293  * This method is disabled for button inputs configured as {@link OO.ui.ButtonInputWidget#useInputTag <input> tags}, as
10294  * they do not support {@link OO.ui.ButtonInputWidget#value values}.
10296  * @param {string} value New value
10297  * @chainable
10298  * @return {OO.ui.Widget} The widget, for chaining
10299  */
10300 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
10301         if ( !this.useInputTag ) {
10302                 OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
10303         }
10304         return this;
10308  * @inheritdoc
10309  */
10310 OO.ui.ButtonInputWidget.prototype.getInputId = function () {
10311         // Disable generating `<label>` elements for buttons. One would very rarely need additional
10312         // label for a button, and it's already a big clickable target, and it causes
10313         // unexpected rendering.
10314         return null;
10318  * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
10319  * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
10320  * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
10321  * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
10323  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10325  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10327  *     @example
10328  *     // An example of selected, unselected, and disabled checkbox inputs.
10329  *     const checkbox1 = new OO.ui.CheckboxInputWidget( {
10330  *             value: 'a',
10331  *              selected: true
10332  *         } ),
10333  *         checkbox2 = new OO.ui.CheckboxInputWidget( {
10334  *             value: 'b'
10335  *         } ),
10336  *         checkbox3 = new OO.ui.CheckboxInputWidget( {
10337  *             value:'c',
10338  *             disabled: true
10339  *         } ),
10340  *         // Create a fieldset layout with fields for each checkbox.
10341  *         fieldset = new OO.ui.FieldsetLayout( {
10342  *             label: 'Checkboxes'
10343  *         } );
10344  *     fieldset.addItems( [
10345  *         new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
10346  *         new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
10347  *         new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
10348  *     ] );
10349  *     $( document.body ).append( fieldset.$element );
10351  * @class
10352  * @extends OO.ui.InputWidget
10354  * @constructor
10355  * @param {Object} [config] Configuration options
10356  * @param {boolean} [config.selected=false] Select the checkbox initially. By default, the checkbox is
10357  *  not selected.
10358  * @param {boolean} [config.indeterminate=false] Whether the checkbox is in the indeterminate state.
10359  */
10360 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
10361         // Configuration initialization
10362         config = config || {};
10364         // Parent constructor
10365         OO.ui.CheckboxInputWidget.super.call( this, config );
10367         // Mixin constructors
10368         OO.ui.mixin.RequiredElement.call( this, Object.assign( {}, {
10369                 // TODO: Display the required indicator somewhere
10370                 indicatorElement: null
10371         }, config ) );
10373         // Properties
10374         this.checkIcon = new OO.ui.IconWidget( {
10375                 icon: 'check',
10376                 classes: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
10377         } );
10379         // Initialization
10380         this.$element
10381                 .addClass( 'oo-ui-checkboxInputWidget' )
10382                 // Required for pretty styling in WikimediaUI theme
10383                 .append( this.checkIcon.$element );
10384         this.setSelected( config.selected !== undefined ? config.selected : false );
10385         this.setIndeterminate( config.indeterminate !== undefined ? config.indeterminate : false );
10388 /* Setup */
10390 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
10391 OO.mixinClass( OO.ui.CheckboxInputWidget, OO.ui.mixin.RequiredElement );
10393 /* Events */
10396  * A change event is emitted when the state of the input changes.
10398  * @event OO.ui.CheckboxInputWidget#change
10399  * @param {boolean} selected
10400  * @param {boolean} indeterminate
10401  */
10403 /* Static Properties */
10406  * @inheritdoc
10407  */
10408 OO.ui.CheckboxInputWidget.static.tagName = 'span';
10410 /* Static Methods */
10413  * @inheritdoc
10414  */
10415 OO.ui.CheckboxInputWidget.static.gatherPreInfuseState = function ( node, config ) {
10416         const state = OO.ui.CheckboxInputWidget.super.static.gatherPreInfuseState( node, config );
10417         if ( config.$input ) {
10418                 state.checked = config.$input.prop( 'checked' );
10419         }
10420         return state;
10423 /* Methods */
10426  * @inheritdoc
10427  * @protected
10428  */
10429 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
10430         return $( '<input>' ).attr( 'type', 'checkbox' );
10434  * @inheritdoc
10435  */
10436 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
10437         if ( !this.isDisabled() ) {
10438                 // Allow the stack to clear so the value will be updated
10439                 setTimeout( () => {
10440                         this.setSelected( this.$input.prop( 'checked' ) );
10441                         this.setIndeterminate( this.$input.prop( 'indeterminate' ) );
10442                 } );
10443         }
10447  * Set selection state of this checkbox.
10449  * @param {boolean} [state=false] Selected state
10450  * @param {boolean} [internal=false] Used for internal calls to suppress events
10451  * @chainable
10452  * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
10453  */
10454 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state, internal ) {
10455         state = !!state;
10456         if ( this.selected !== state ) {
10457                 this.selected = state;
10458                 this.$input.prop( 'checked', this.selected );
10459                 if ( !internal ) {
10460                         this.setIndeterminate( false, true );
10461                         this.emit( 'change', this.selected, this.indeterminate );
10462                 }
10463         }
10464         // The first time that the selection state is set (probably while constructing the widget),
10465         // remember it in defaultSelected. This property can be later used to check whether
10466         // the selection state of the input has been changed since it was created.
10467         if ( this.defaultSelected === undefined ) {
10468                 this.defaultSelected = this.selected;
10469                 this.$input[ 0 ].defaultChecked = this.defaultSelected;
10470         }
10471         return this;
10475  * Check if this checkbox is selected.
10477  * @return {boolean} Checkbox is selected
10478  */
10479 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
10480         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10481         // it, and we won't know unless they're kind enough to trigger a 'change' event.
10482         const selected = this.$input.prop( 'checked' );
10483         if ( this.selected !== selected ) {
10484                 this.setSelected( selected );
10485         }
10486         return this.selected;
10490  * Set indeterminate state of this checkbox.
10492  * @param {boolean} [state=false] Indeterminate state
10493  * @param {boolean} [internal=false] Used for internal calls to suppress events
10494  * @chainable
10495  * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
10496  */
10497 OO.ui.CheckboxInputWidget.prototype.setIndeterminate = function ( state, internal ) {
10498         state = !!state;
10499         if ( this.indeterminate !== state ) {
10500                 this.indeterminate = state;
10501                 this.$input.prop( 'indeterminate', this.indeterminate );
10502                 if ( !internal ) {
10503                         this.setSelected( false, true );
10504                         this.emit( 'change', this.selected, this.indeterminate );
10505                 }
10506         }
10507         return this;
10511  * Check if this checkbox is selected.
10513  * @return {boolean} Checkbox is selected
10514  */
10515 OO.ui.CheckboxInputWidget.prototype.isIndeterminate = function () {
10516         // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10517         // it, and we won't know unless they're kind enough to trigger a 'change' event.
10518         const indeterminate = this.$input.prop( 'indeterminate' );
10519         if ( this.indeterminate !== indeterminate ) {
10520                 this.setIndeterminate( indeterminate );
10521         }
10522         return this.indeterminate;
10526  * @inheritdoc
10527  */
10528 OO.ui.CheckboxInputWidget.prototype.simulateLabelClick = function () {
10529         if ( !this.isDisabled() ) {
10530                 this.$handle.trigger( 'click' );
10531         }
10532         this.focus();
10536  * @inheritdoc
10537  */
10538 OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
10539         OO.ui.CheckboxInputWidget.super.prototype.restorePreInfuseState.call( this, state );
10540         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
10541                 this.setSelected( state.checked );
10542         }
10546  * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
10547  * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the
10548  * value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
10549  * more information about input widgets.
10551  * A DropdownInputWidget always has a value (one of the options is always selected), unless there
10552  * are no options. If no `value` configuration option is provided, the first option is selected.
10553  * If you need a state representing no value (no option being selected), use a DropdownWidget.
10555  * This and OO.ui.RadioSelectInputWidget support similar configuration options.
10557  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10559  *     @example
10560  *     // A DropdownInputWidget with three options.
10561  *     const dropdownInput = new OO.ui.DropdownInputWidget( {
10562  *         options: [
10563  *             { data: 'a', label: 'First' },
10564  *             { data: 'b', label: 'Second', disabled: true },
10565  *             { optgroup: 'Group label' },
10566  *             { data: 'c', label: 'First sub-item)' }
10567  *         ]
10568  *     } );
10569  *     $( document.body ).append( dropdownInput.$element );
10571  * @class
10572  * @extends OO.ui.InputWidget
10574  * @constructor
10575  * @param {Object} [config] Configuration options
10576  * @param {Object[]} [config.options=[]] Array of menu options in the format described above.
10577  * @param {Object} [config.dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
10578  * @param {jQuery|boolean} [config.$overlay] Render the menu into a separate layer. This configuration is
10579  *  useful in cases where the expanded menu is larger than its containing `<div>`. The specified
10580  *  overlay layer is usually on top of the containing `<div>` and has a larger area. By default,
10581  *  the menu uses relative positioning. Pass 'true' to use the default overlay.
10582  *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
10583  */
10584 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
10585         // Configuration initialization
10586         config = config || {};
10588         // Properties (must be done before parent constructor which calls #setDisabled)
10589         this.dropdownWidget = new OO.ui.DropdownWidget( Object.assign(
10590                 {
10591                         $overlay: config.$overlay
10592                 },
10593                 config.dropdown
10594         ) );
10595         // Set up the options before parent constructor, which uses them to validate config.value.
10596         // Use this instead of setOptions() because this.$input is not set up yet.
10597         this.setOptionsData( config.options || [] );
10599         // Parent constructor
10600         OO.ui.DropdownInputWidget.super.call( this, config );
10602         // Mixin constructors
10603         OO.ui.mixin.RequiredElement.call( this, Object.assign( {}, {
10604                 // TODO: Display the required indicator somewhere
10605                 indicatorElement: null
10606         }, config ) );
10608         // Events
10609         this.dropdownWidget.getMenu().connect( this, {
10610                 select: 'onMenuSelect'
10611         } );
10613         // Initialization
10614         this.$element
10615                 .addClass( 'oo-ui-dropdownInputWidget' )
10616                 .append( this.dropdownWidget.$element );
10617         if ( OO.ui.isMobile() ) {
10618                 this.$element.addClass( 'oo-ui-isMobile' );
10619         }
10620         this.setTabIndexedElement( this.dropdownWidget.$tabIndexed );
10621         this.setTitledElement( this.dropdownWidget.$handle );
10624 /* Setup */
10626 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
10627 OO.mixinClass( OO.ui.DropdownInputWidget, OO.ui.mixin.RequiredElement );
10629 /* Methods */
10632  * @inheritdoc
10633  * @protected
10634  */
10635 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
10636         return $( '<select>' ).addClass( 'oo-ui-indicator-down' );
10640  * Handles menu select events.
10642  * @private
10643  * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
10644  */
10645 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
10646         this.setValue( item ? item.getData() : '' );
10650  * @inheritdoc
10651  */
10652 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
10653         value = this.cleanUpValue( value );
10654         // Only allow setting values that are actually present in the dropdown
10655         const selected = this.dropdownWidget.getMenu().findItemFromData( value ) ||
10656                 this.dropdownWidget.getMenu().findFirstSelectableItem();
10657         this.dropdownWidget.getMenu().selectItem( selected );
10658         value = selected ? selected.getData() : '';
10659         OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
10660         if ( this.optionsDirty ) {
10661                 // We reached this from the constructor or from #setOptions.
10662                 // We have to update the <select> element.
10663                 this.updateOptionsInterface();
10664         }
10665         return this;
10669  * @inheritdoc
10670  */
10671 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
10672         this.dropdownWidget.setDisabled( state );
10673         OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
10674         return this;
10678  * Set the options available for this input.
10680  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10681  * @chainable
10682  * @return {OO.ui.Widget} The widget, for chaining
10683  */
10684 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
10685         const value = this.getValue();
10687         this.setOptionsData( options );
10689         // Re-set the value to update the visible interface (DropdownWidget and <select>).
10690         // In case the previous value is no longer an available option, select the first valid one.
10691         this.setValue( value );
10693         return this;
10697  * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10699  * This method may be called before the parent constructor, so various properties may not be
10700  * initialized yet.
10702  * @param {Object[]} options Array of menu options (see #constructor for details).
10703  * @private
10704  */
10705 OO.ui.DropdownInputWidget.prototype.setOptionsData = function ( options ) {
10706         this.optionsDirty = true;
10708         // Go through all the supplied option configs and create either
10709         // MenuSectionOption or MenuOption widgets from each.
10710         const optionWidgets = [];
10711         let previousOptgroup;
10712         for ( let optIndex = 0; optIndex < options.length; optIndex++ ) {
10713                 const opt = options[ optIndex ];
10715                 let optionWidget;
10716                 if ( opt.optgroup !== undefined ) {
10717                         // Create a <optgroup> menu item.
10718                         optionWidget = this.createMenuSectionOptionWidget( opt.optgroup );
10719                         previousOptgroup = optionWidget;
10721                 } else {
10722                         // Create a normal <option> menu item.
10723                         const optValue = this.cleanUpValue( opt.data );
10724                         optionWidget = this.createMenuOptionWidget(
10725                                 optValue,
10726                                 opt.label !== undefined ? opt.label : optValue
10727                         );
10728                 }
10730                 // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
10731                 if (
10732                         opt.disabled !== undefined ||
10733                         previousOptgroup instanceof OO.ui.MenuSectionOptionWidget &&
10734                         previousOptgroup.isDisabled()
10735                 ) {
10736                         optionWidget.setDisabled( true );
10737                 }
10739                 optionWidgets.push( optionWidget );
10740         }
10742         this.dropdownWidget.getMenu().clearItems().addItems( optionWidgets );
10746  * Create a menu option widget.
10748  * @protected
10749  * @param {string} data Item data
10750  * @param {string} label Item label
10751  * @return {OO.ui.MenuOptionWidget} Option widget
10752  */
10753 OO.ui.DropdownInputWidget.prototype.createMenuOptionWidget = function ( data, label ) {
10754         return new OO.ui.MenuOptionWidget( {
10755                 data: data,
10756                 label: label
10757         } );
10761  * Create a menu section option widget.
10763  * @protected
10764  * @param {string} label Section item label
10765  * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
10766  */
10767 OO.ui.DropdownInputWidget.prototype.createMenuSectionOptionWidget = function ( label ) {
10768         return new OO.ui.MenuSectionOptionWidget( {
10769                 label: label
10770         } );
10774  * Update the user-visible interface to match the internal list of options and value.
10776  * This method must only be called after the parent constructor.
10778  * @private
10779  */
10780 OO.ui.DropdownInputWidget.prototype.updateOptionsInterface = function () {
10781         let $optionsContainer = this.$input;
10783         const defaultValue = this.defaultValue;
10785         this.$input.empty();
10787         this.dropdownWidget.getMenu().getItems().forEach( ( optionWidget ) => {
10788                 let $optionNode;
10790                 if ( !( optionWidget instanceof OO.ui.MenuSectionOptionWidget ) ) {
10791                         $optionNode = $( '<option>' )
10792                                 .attr( 'value', optionWidget.getData() )
10793                                 .text( optionWidget.getLabel() );
10795                         // Remember original selection state. This property can be later used to check whether
10796                         // the selection state of the input has been changed since it was created.
10797                         $optionNode[ 0 ].defaultSelected = ( optionWidget.getData() === defaultValue );
10799                         $optionsContainer.append( $optionNode );
10800                 } else {
10801                         $optionNode = $( '<optgroup>' )
10802                                 .attr( 'label', optionWidget.getLabel() );
10803                         this.$input.append( $optionNode );
10804                         $optionsContainer = $optionNode;
10805                 }
10807                 // Disable the option or optgroup if required.
10808                 if ( optionWidget.isDisabled() ) {
10809                         $optionNode.prop( 'disabled', true );
10810                 }
10811         } );
10813         this.optionsDirty = false;
10817  * @inheritdoc
10818  */
10819 OO.ui.DropdownInputWidget.prototype.focus = function () {
10820         this.dropdownWidget.focus();
10821         return this;
10825  * @inheritdoc
10826  */
10827 OO.ui.DropdownInputWidget.prototype.blur = function () {
10828         this.dropdownWidget.blur();
10829         return this;
10833  * @inheritdoc
10834  */
10835 OO.ui.DropdownInputWidget.prototype.setLabelledBy = function ( id ) {
10836         this.dropdownWidget.setLabelledBy( id );
10840  * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
10841  * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
10842  * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
10843  * please see the [OOUI documentation on MediaWiki][1].
10845  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10847  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10849  *     @example
10850  *     // An example of selected, unselected, and disabled radio inputs
10851  *     const radio1 = new OO.ui.RadioInputWidget( {
10852  *         value: 'a',
10853  *         selected: true
10854  *     } );
10855  *     const radio2 = new OO.ui.RadioInputWidget( {
10856  *         value: 'b'
10857  *     } );
10858  *     const radio3 = new OO.ui.RadioInputWidget( {
10859  *         value: 'c',
10860  *         disabled: true
10861  *     } );
10862  *     // Create a fieldset layout with fields for each radio button.
10863  *     const fieldset = new OO.ui.FieldsetLayout( {
10864  *         label: 'Radio inputs'
10865  *     } );
10866  *     fieldset.addItems( [
10867  *         new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
10868  *         new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
10869  *         new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
10870  *     ] );
10871  *     $( document.body ).append( fieldset.$element );
10873  * @class
10874  * @extends OO.ui.InputWidget
10876  * @constructor
10877  * @param {Object} [config] Configuration options
10878  * @param {boolean} [config.selected=false] Select the radio button initially. By default, the radio button
10879  *  is not selected.
10880  */
10881 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
10882         // Configuration initialization
10883         config = config || {};
10885         // Parent constructor
10886         OO.ui.RadioInputWidget.super.call( this, config );
10888         // Mixin constructors
10889         OO.ui.mixin.RequiredElement.call( this, Object.assign( {}, {
10890                 // TODO: Display the required indicator somewhere
10891                 indicatorElement: null
10892         }, config ) );
10894         // Initialization
10895         this.$element
10896                 .addClass( 'oo-ui-radioInputWidget' )
10897                 // Required for pretty styling in WikimediaUI theme
10898                 .append( $( '<span>' ) );
10899         this.setSelected( config.selected !== undefined ? config.selected : false );
10902 /* Setup */
10904 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
10905 OO.mixinClass( OO.ui.RadioInputWidget, OO.ui.mixin.RequiredElement );
10907 /* Static Properties */
10910  * @static
10911  * @inheritdoc
10912  */
10913 OO.ui.RadioInputWidget.static.tagName = 'span';
10915 /* Static Methods */
10918  * @inheritdoc
10919  */
10920 OO.ui.RadioInputWidget.static.gatherPreInfuseState = function ( node, config ) {
10921         const state = OO.ui.RadioInputWidget.super.static.gatherPreInfuseState( node, config );
10922         if ( config.$input ) {
10923                 state.checked = config.$input.prop( 'checked' );
10924         }
10925         return state;
10928 /* Methods */
10931  * @inheritdoc
10932  * @protected
10933  */
10934 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
10935         return $( '<input>' ).attr( 'type', 'radio' );
10939  * @inheritdoc
10940  */
10941 OO.ui.RadioInputWidget.prototype.onEdit = function () {
10942         // RadioInputWidget doesn't track its state.
10946  * Set selection state of this radio button.
10948  * @param {boolean} state `true` for selected
10949  * @chainable
10950  * @return {OO.ui.Widget} The widget, for chaining
10951  */
10952 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
10953         // RadioInputWidget doesn't track its state.
10954         this.$input.prop( 'checked', state );
10955         // The first time that the selection state is set (probably while constructing the widget),
10956         // remember it in defaultSelected. This property can be later used to check whether
10957         // the selection state of the input has been changed since it was created.
10958         if ( this.defaultSelected === undefined ) {
10959                 this.defaultSelected = state;
10960                 this.$input[ 0 ].defaultChecked = this.defaultSelected;
10961         }
10962         return this;
10966  * Check if this radio button is selected.
10968  * @return {boolean} Radio is selected
10969  */
10970 OO.ui.RadioInputWidget.prototype.isSelected = function () {
10971         return this.$input.prop( 'checked' );
10975  * @inheritdoc
10976  */
10977 OO.ui.RadioInputWidget.prototype.simulateLabelClick = function () {
10978         if ( !this.isDisabled() ) {
10979                 this.$input.trigger( 'click' );
10980         }
10981         this.focus();
10985  * @inheritdoc
10986  */
10987 OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
10988         OO.ui.RadioInputWidget.super.prototype.restorePreInfuseState.call( this, state );
10989         if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
10990                 this.setSelected( state.checked );
10991         }
10995  * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be
10996  * used within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with
10997  * the value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
10998  * more information about input widgets.
11000  * This and OO.ui.DropdownInputWidget support similar configuration options.
11002  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
11004  *     @example
11005  *     // A RadioSelectInputWidget with three options
11006  *     const radioSelectInput = new OO.ui.RadioSelectInputWidget( {
11007  *         options: [
11008  *             { data: 'a', label: 'First' },
11009  *             { data: 'b', label: 'Second'},
11010  *             { data: 'c', label: 'Third' }
11011  *         ]
11012  *     } );
11013  *     $( document.body ).append( radioSelectInput.$element );
11015  * @class
11016  * @extends OO.ui.InputWidget
11018  * @constructor
11019  * @param {Object} [config] Configuration options
11020  * @param {Object[]} [config.options=[]] Array of menu options in the format `{ data: …, label: … }`
11021  */
11022 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
11023         // Configuration initialization
11024         config = config || {};
11026         // Properties (must be done before parent constructor which calls #setDisabled)
11027         this.radioSelectWidget = new OO.ui.RadioSelectWidget();
11028         // Set up the options before parent constructor, which uses them to validate config.value.
11029         // Use this instead of setOptions() because this.$input is not set up yet
11030         this.setOptionsData( config.options || [] );
11032         // Parent constructor
11033         OO.ui.RadioSelectInputWidget.super.call( this, config );
11035         // Events
11036         this.radioSelectWidget.connect( this, {
11037                 select: 'onMenuSelect'
11038         } );
11040         // Initialization
11041         this.$element
11042                 .addClass( 'oo-ui-radioSelectInputWidget' )
11043                 .append( this.radioSelectWidget.$element );
11044         this.setTabIndexedElement( this.radioSelectWidget.$tabIndexed );
11047 /* Setup */
11049 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
11051 /* Static Methods */
11054  * @inheritdoc
11055  */
11056 OO.ui.RadioSelectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
11057         const state = OO.ui.RadioSelectInputWidget.super.static.gatherPreInfuseState( node, config );
11058         state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
11059         return state;
11063  * @inheritdoc
11064  */
11065 OO.ui.RadioSelectInputWidget.static.reusePreInfuseDOM = function ( node, config ) {
11066         config = OO.ui.RadioSelectInputWidget.super.static.reusePreInfuseDOM( node, config );
11067         // Cannot reuse the `<input type=radio>` set
11068         delete config.$input;
11069         return config;
11072 /* Methods */
11075  * @inheritdoc
11076  * @protected
11077  */
11078 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
11079         // Use this instead of <input type="hidden">, because hidden inputs do not have separate
11080         // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
11081         return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
11085  * Handles menu select events.
11087  * @private
11088  * @param {OO.ui.RadioOptionWidget} item Selected menu item
11089  */
11090 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
11091         this.setValue( item.getData() );
11095  * @inheritdoc
11096  */
11097 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
11098         value = this.cleanUpValue( value );
11099         // Only allow setting values that are actually present in the dropdown
11100         const selected = this.radioSelectWidget.findItemFromData( value ) ||
11101                 this.radioSelectWidget.findFirstSelectableItem();
11102         this.radioSelectWidget.selectItem( selected );
11103         value = selected ? selected.getData() : '';
11104         OO.ui.RadioSelectInputWidget.super.prototype.setValue.call( this, value );
11105         return this;
11109  * @inheritdoc
11110  */
11111 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
11112         this.radioSelectWidget.setDisabled( state );
11113         OO.ui.RadioSelectInputWidget.super.prototype.setDisabled.call( this, state );
11114         return this;
11118  * Set the options available for this input.
11120  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11121  * @chainable
11122  * @return {OO.ui.Widget} The widget, for chaining
11123  */
11124 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
11125         const value = this.getValue();
11127         this.setOptionsData( options );
11129         // Re-set the value to update the visible interface (RadioSelectWidget).
11130         // In case the previous value is no longer an available option, select the first valid one.
11131         this.setValue( value );
11133         return this;
11137  * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
11139  * This method may be called before the parent constructor, so various properties may not be
11140  * initialized yet.
11142  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11143  * @private
11144  */
11145 OO.ui.RadioSelectInputWidget.prototype.setOptionsData = function ( options ) {
11146         this.radioSelectWidget
11147                 .clearItems()
11148                 .addItems( options.map( ( opt ) => {
11149                         const optValue = this.cleanUpValue( opt.data );
11150                         return new OO.ui.RadioOptionWidget( {
11151                                 data: optValue,
11152                                 label: opt.label !== undefined ? opt.label : optValue
11153                         } );
11154                 } ) );
11158  * @inheritdoc
11159  */
11160 OO.ui.RadioSelectInputWidget.prototype.focus = function () {
11161         this.radioSelectWidget.focus();
11162         return this;
11166  * @inheritdoc
11167  */
11168 OO.ui.RadioSelectInputWidget.prototype.blur = function () {
11169         this.radioSelectWidget.blur();
11170         return this;
11174  * CheckboxMultiselectInputWidget is a
11175  * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
11176  * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
11177  * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
11178  * more information about input widgets.
11180  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
11182  *     @example
11183  *     // A CheckboxMultiselectInputWidget with three options.
11184  *     const multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
11185  *         options: [
11186  *             { data: 'a', label: 'First' },
11187  *             { data: 'b', label: 'Second' },
11188  *             { data: 'c', label: 'Third' }
11189  *         ]
11190  *     } );
11191  *     $( document.body ).append( multiselectInput.$element );
11193  * @class
11194  * @extends OO.ui.InputWidget
11196  * @constructor
11197  * @param {Object} [config] Configuration options
11198  * @param {Object[]} [config.options=[]] Array of menu options in the format
11199  *  `{ data: …, label: …, disabled: … }`
11200  */
11201 OO.ui.CheckboxMultiselectInputWidget = function OoUiCheckboxMultiselectInputWidget( config ) {
11202         // Configuration initialization
11203         config = config || {};
11205         // Properties (must be done before parent constructor which calls #setDisabled)
11206         this.checkboxMultiselectWidget = new OO.ui.CheckboxMultiselectWidget();
11207         // Must be set before the #setOptionsData call below
11208         this.inputName = config.name;
11209         // Set up the options before parent constructor, which uses them to validate config.value.
11210         // Use this instead of setOptions() because this.$input is not set up yet
11211         this.setOptionsData( config.options || [] );
11213         // Parent constructor
11214         OO.ui.CheckboxMultiselectInputWidget.super.call( this, config );
11216         // Events
11217         // HACK: When selecting multiple items, the 'select' event is fired after every item, and our
11218         // handler performs a linear-time operation (validating every selected value), so selecting
11219         // multiple items becomes quadratic (T335082#8815547). Debounce it, so that it only executes
11220         // once at the end of the selecting, making it linear again. This will make the internal state
11221         // momentarily inconsistent while the selecting is ongoing, but that's probably fine.
11222         this.onCheckboxesSelectHandler = OO.ui.debounce( this.onCheckboxesSelect );
11223         this.checkboxMultiselectWidget.connect( this, {
11224                 select: 'onCheckboxesSelectHandler'
11225         } );
11227         // Initialization
11228         this.$element
11229                 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
11230                 .append( this.checkboxMultiselectWidget.$element );
11231         // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
11232         this.$input.detach();
11235 /* Setup */
11237 OO.inheritClass( OO.ui.CheckboxMultiselectInputWidget, OO.ui.InputWidget );
11239 /* Static Methods */
11242  * @inheritdoc
11243  */
11244 OO.ui.CheckboxMultiselectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
11245         const state = OO.ui.CheckboxMultiselectInputWidget.super.static.gatherPreInfuseState(
11246                 node, config
11247         );
11248         state.value = $( node ).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
11249                 .toArray().map( ( el ) => el.value );
11250         return state;
11254  * @inheritdoc
11255  */
11256 OO.ui.CheckboxMultiselectInputWidget.static.reusePreInfuseDOM = function ( node, config ) {
11257         config = OO.ui.CheckboxMultiselectInputWidget.super.static.reusePreInfuseDOM( node, config );
11258         // Cannot reuse the `<input type=checkbox>` set
11259         delete config.$input;
11260         return config;
11263 /* Methods */
11266  * @inheritdoc
11267  * @protected
11268  */
11269 OO.ui.CheckboxMultiselectInputWidget.prototype.getInputElement = function () {
11270         // Actually unused
11271         return $( '<unused>' );
11275  * Handles CheckboxMultiselectWidget select events.
11277  * @private
11278  */
11279 OO.ui.CheckboxMultiselectInputWidget.prototype.onCheckboxesSelect = function () {
11280         this.setValue( this.checkboxMultiselectWidget.findSelectedItemsData() );
11284  * @inheritdoc
11285  */
11286 OO.ui.CheckboxMultiselectInputWidget.prototype.getValue = function () {
11287         const value = this.$element.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
11288                 .toArray().map( ( el ) => el.value );
11289         if ( !OO.compare( this.value, value ) ) {
11290                 this.setValue( value );
11291         }
11292         return this.value;
11296  * @inheritdoc
11297  */
11298 OO.ui.CheckboxMultiselectInputWidget.prototype.setValue = function ( value ) {
11299         value = this.cleanUpValue( value );
11300         this.checkboxMultiselectWidget.selectItemsByData( value );
11301         OO.ui.CheckboxMultiselectInputWidget.super.prototype.setValue.call( this, value );
11302         if ( this.optionsDirty ) {
11303                 // We reached this from the constructor or from #setOptions.
11304                 // We have to update the <select> element.
11305                 this.updateOptionsInterface();
11306         }
11307         return this;
11311  * Clean up incoming value.
11313  * @param {string[]} value Original value
11314  * @return {string[]} Cleaned up value
11315  */
11316 OO.ui.CheckboxMultiselectInputWidget.prototype.cleanUpValue = function ( value ) {
11317         const cleanValue = [];
11318         if ( !Array.isArray( value ) ) {
11319                 return cleanValue;
11320         }
11321         const dataHashSet = new Set( this.checkboxMultiselectWidget.getItems().map( ( item ) => OO.getHash( item.getData() ) ) );
11322         for ( let i = 0; i < value.length; i++ ) {
11323                 const singleValue = OO.ui.CheckboxMultiselectInputWidget.super.prototype.cleanUpValue
11324                         .call( this, value[ i ] );
11325                 // Remove options that we don't have here
11326                 if ( !dataHashSet.has( OO.getHash( singleValue ) ) ) {
11327                         continue;
11328                 }
11329                 cleanValue.push( singleValue );
11330         }
11331         return cleanValue;
11335  * @inheritdoc
11336  */
11337 OO.ui.CheckboxMultiselectInputWidget.prototype.setDisabled = function ( state ) {
11338         this.checkboxMultiselectWidget.setDisabled( state );
11339         OO.ui.CheckboxMultiselectInputWidget.super.prototype.setDisabled.call( this, state );
11340         return this;
11344  * Set the options available for this input.
11346  * @param {Object[]} options Array of menu options in the format
11347  *  `{ data: …, label: …, disabled: … }`
11348  * @chainable
11349  * @return {OO.ui.Widget} The widget, for chaining
11350  */
11351 OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options ) {
11352         const value = this.getValue();
11354         this.setOptionsData( options );
11356         // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
11357         // This will also get rid of any stale options that we just removed.
11358         this.setValue( value );
11360         return this;
11364  * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
11366  * This method may be called before the parent constructor, so various properties may not be
11367  * initialized yet.
11369  * @param {Object[]} options Array of menu options in the format
11370  *  `{ data: …, label: … }`
11371  * @private
11372  */
11373 OO.ui.CheckboxMultiselectInputWidget.prototype.setOptionsData = function ( options ) {
11374         this.optionsDirty = true;
11376         this.checkboxMultiselectWidget
11377                 .clearItems()
11378                 .addItems( options.map( ( opt ) => {
11379                         const optValue = OO.ui.CheckboxMultiselectInputWidget.super.prototype.cleanUpValue
11380                                 .call( this, opt.data );
11381                         const optDisabled = opt.disabled !== undefined ? opt.disabled : false;
11382                         const item = new OO.ui.CheckboxMultioptionWidget( {
11383                                 data: optValue,
11384                                 label: opt.label !== undefined ? opt.label : optValue,
11385                                 disabled: optDisabled
11386                         } );
11387                         // Set the 'name' and 'value' for form submission
11388                         item.checkbox.$input.attr( 'name', this.inputName );
11389                         item.checkbox.setValue( optValue );
11390                         return item;
11391                 } ) );
11395  * Update the user-visible interface to match the internal list of options and value.
11397  * This method must only be called after the parent constructor.
11399  * @private
11400  */
11401 OO.ui.CheckboxMultiselectInputWidget.prototype.updateOptionsInterface = function () {
11402         const defaultValueSet = new Set( this.defaultValue );
11404         this.checkboxMultiselectWidget.getItems().forEach( ( item ) => {
11405                 // Remember original selection state. This property can be later used to check whether
11406                 // the selection state of the input has been changed since it was created.
11407                 const isDefault = defaultValueSet.has( item.getData() );
11408                 item.checkbox.defaultSelected = isDefault;
11409                 item.checkbox.$input[ 0 ].defaultChecked = isDefault;
11410         } );
11412         this.optionsDirty = false;
11416  * @inheritdoc
11417  */
11418 OO.ui.CheckboxMultiselectInputWidget.prototype.focus = function () {
11419         this.checkboxMultiselectWidget.focus();
11420         return this;
11424  * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
11425  * size of the field as well as its presentation. In addition, these widgets can be configured
11426  * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an
11427  * optional validation-pattern (used to determine if an input value is valid or not) and an input
11428  * filter, which modifies incoming values rather than validating them.
11429  * Please see the [OOUI documentation on MediaWiki][1] for more information and examples.
11431  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11433  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
11435  *     @example
11436  *     // A TextInputWidget.
11437  *     const textInput = new OO.ui.TextInputWidget( {
11438  *         value: 'Text input'
11439  *     } );
11440  *     $( document.body ).append( textInput.$element );
11442  * @class
11443  * @extends OO.ui.InputWidget
11444  * @mixes OO.ui.mixin.IconElement
11445  * @mixes OO.ui.mixin.IndicatorElement
11446  * @mixes OO.ui.mixin.PendingElement
11447  * @mixes OO.ui.mixin.LabelElement
11448  * @mixes OO.ui.mixin.FlaggedElement
11450  * @constructor
11451  * @param {Object} [config] Configuration options
11452  * @param {string} [config.type='text'] The value of the HTML `type` attribute: 'text', 'password'
11453  *  'email', 'url' or 'number'. Subclasses might support other types.
11454  * @param {string} [config.placeholder] Placeholder text
11455  * @param {boolean} [config.autofocus=false] Use an HTML `autofocus` attribute to
11456  *  instruct the browser to focus this widget.
11457  * @param {boolean} [config.readOnly=false] Prevent changes to the value of the text input.
11458  * @param {number} [config.maxLength] Maximum number of characters allowed in the input.
11459  * @param {number} [config.minLength] Minimum number of characters allowed in the input.
11461  *  For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
11462  *  Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
11463  *  many emojis) count as 2 characters each.
11464  * @param {string} [config.labelPosition='after'] The position of the inline label relative to that of
11465  *  the value or placeholder text: `'before'` or `'after'`
11466  * @param {boolean|string} [config.autocomplete] Should the browser support autocomplete for this field?
11467  *  Type hints such as 'email' are also allowed.
11468  * @param {boolean} [config.spellcheck] Should the browser support spellcheck for this field (`undefined`
11469  *  means leaving it up to the browser).
11470  * @param {RegExp|Function|string} [config.validate] Validation pattern: when string, a symbolic name of a
11471  *  pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
11472  *  (the value must contain only numbers); when RegExp, a regular expression that must match the
11473  *  value for it to be considered valid; when Function, a function receiving the value as parameter
11474  *  that must return true, or promise resolving to true, for it to be considered valid.
11475  */
11476 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
11477         // Configuration initialization
11478         config = Object.assign( {
11479                 labelPosition: 'after'
11480         }, config );
11481         config.type = this.getValidType( config );
11482         if ( config.autocomplete === false ) {
11483                 config.autocomplete = 'off';
11484         } else if ( config.autocomplete === true ) {
11485                 config.autocomplete = 'on';
11486         }
11488         // Parent constructor
11489         OO.ui.TextInputWidget.super.call( this, config );
11491         // Mixin constructors
11492         OO.ui.mixin.IconElement.call( this, config );
11493         OO.ui.mixin.IndicatorElement.call( this, config );
11494         OO.ui.mixin.PendingElement.call( this, Object.assign( { $pending: this.$input }, config ) );
11495         OO.ui.mixin.LabelElement.call( this, config );
11496         OO.ui.mixin.FlaggedElement.call( this, config );
11497         OO.ui.mixin.RequiredElement.call( this, config );
11499         // Properties
11500         this.type = config.type;
11501         this.readOnly = false;
11502         this.validate = null;
11503         this.scrollWidth = null;
11505         this.setValidation( config.validate );
11506         this.setLabelPosition( config.labelPosition );
11508         // Events
11509         this.$input.on( {
11510                 keypress: this.onKeyPress.bind( this ),
11511                 blur: this.onBlur.bind( this ),
11512                 focus: this.onFocus.bind( this )
11513         } );
11514         this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
11515         this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
11516         this.on( 'labelChange', this.updatePosition.bind( this ) );
11517         this.on( 'change', OO.ui.debounce( this.onDebouncedChange.bind( this ), 250 ) );
11519         // Initialization
11520         this.$element
11521                 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + config.type )
11522                 .append( this.$icon, this.$indicator );
11523         this.setReadOnly( !!config.readOnly );
11524         if ( config.placeholder !== undefined ) {
11525                 this.$input.attr( 'placeholder', config.placeholder );
11526         }
11527         if ( config.maxLength !== undefined ) {
11528                 this.$input.attr( 'maxlength', config.maxLength );
11529         }
11530         if ( config.minLength !== undefined ) {
11531                 this.$input.attr( 'minlength', config.minLength );
11532         }
11533         if ( config.autofocus ) {
11534                 this.$input.attr( 'autofocus', 'autofocus' );
11535         }
11536         if ( config.autocomplete !== null && config.autocomplete !== undefined ) {
11537                 this.$input.attr( 'autocomplete', config.autocomplete );
11538                 if ( config.autocomplete === 'off' ) {
11539                         // Turning off autocompletion also disables "form caching" when the user navigates to a
11540                         // different page and then clicks "Back". Re-enable it when leaving.
11541                         // Borrowed from jQuery UI.
11542                         $( window ).on( {
11543                                 beforeunload: function () {
11544                                         this.$input.removeAttr( 'autocomplete' );
11545                                 }.bind( this ),
11546                                 pageshow: function () {
11547                                         // Browsers don't seem to actually fire this event on "Back", they instead just
11548                                         // reload the whole page... it shouldn't hurt, though.
11549                                         this.$input.attr( 'autocomplete', 'off' );
11550                                 }.bind( this )
11551                         } );
11552                 }
11553         }
11554         if ( config.spellcheck !== undefined ) {
11555                 this.$input.attr( 'spellcheck', config.spellcheck ? 'true' : 'false' );
11556         }
11557         if ( this.label ) {
11558                 this.isWaitingToBeAttached = true;
11559                 this.installParentChangeDetector();
11560         }
11563 /* Setup */
11565 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
11566 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
11567 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
11568 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
11569 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
11570 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.FlaggedElement );
11571 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.RequiredElement );
11573 /* Static Properties */
11575 OO.ui.TextInputWidget.static.validationPatterns = {
11576         'non-empty': /.+/,
11577         integer: /^\d+$/
11580 /* Events */
11583  * An `enter` event is emitted when the user presses Enter key inside the text box.
11585  * @event OO.ui.TextInputWidget#enter
11586  */
11588 /* Methods */
11591  * Focus the input element when clicking on the icon.
11593  * @private
11594  * @param {jQuery.Event} e Mouse down event
11595  * @return {undefined|boolean} False to prevent default if event is handled
11596  */
11597 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
11598         if ( e.which === OO.ui.MouseButtons.LEFT ) {
11599                 this.focus();
11600                 return false;
11601         }
11605  * Focus the input element when clicking on the indicator. This default implementation is
11606  * effectively only suitable for the 'required' indicator. If you are looking for functional 'clear'
11607  * or 'down' indicators, you might want to use the {@link OO.ui.SearchInputWidget SearchInputWidget}
11608  * or {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget} subclasses.
11610  * @private
11611  * @param {jQuery.Event} e Mouse down event
11612  * @return {undefined|boolean} False to prevent default if event is handled
11613  */
11614 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
11615         if ( e.which === OO.ui.MouseButtons.LEFT ) {
11616                 this.focus();
11617                 return false;
11618         }
11622  * Handle key press events.
11624  * @private
11625  * @param {jQuery.Event} e Key press event
11626  * @fires OO.ui.TextInputWidget#enter If Enter key is pressed
11627  */
11628 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
11629         if ( e.which === OO.ui.Keys.ENTER ) {
11630                 this.emit( 'enter', e );
11631         }
11635  * Handle blur events.
11637  * @private
11638  * @param {jQuery.Event} e Blur event
11639  */
11640 OO.ui.TextInputWidget.prototype.onBlur = function () {
11641         this.setValidityFlag();
11645  * Handle focus events.
11647  * @private
11648  * @param {jQuery.Event} e Focus event
11649  */
11650 OO.ui.TextInputWidget.prototype.onFocus = function () {
11651         if ( this.isWaitingToBeAttached ) {
11652                 // If we've received focus, then we must be attached to the document, and if
11653                 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
11654                 this.onElementAttach();
11655         }
11656         this.setValidityFlag( true );
11660  * Handle element attach events.
11662  * @private
11663  * @param {jQuery.Event} e Element attach event
11664  */
11665 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
11666         this.isWaitingToBeAttached = false;
11667         // Any previously calculated size is now probably invalid if we reattached elsewhere
11668         this.valCache = null;
11669         this.positionLabel();
11673  * Handle debounced change events.
11675  * @param {string} value
11676  * @private
11677  */
11678 OO.ui.TextInputWidget.prototype.onDebouncedChange = function () {
11679         this.setValidityFlag();
11683  * Check if the input is {@link OO.ui.TextInputWidget#readOnly read-only}.
11685  * @return {boolean}
11686  */
11687 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
11688         return this.readOnly;
11692  * Set the {@link OO.ui.TextInputWidget#readOnly read-only} state of the input.
11694  * @param {boolean} [state=false] Make input read-only
11695  * @chainable
11696  * @return {OO.ui.Widget} The widget, for chaining
11697  */
11698 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
11699         this.readOnly = !!state;
11700         this.$input.prop( 'readOnly', this.readOnly );
11701         return this;
11705  * Support function for making #onElementAttach work.
11706  */
11707 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
11708         this.connectDetectorNode = document.createElement( 'ooui-connect-detector' );
11709         this.connectDetectorNode.onConnectOOUI = () => {
11710                 if ( this.isElementAttached() ) {
11711                         this.onElementAttach();
11712                 }
11713         };
11715         this.$element.append( this.connectDetectorNode );
11719  * @inheritdoc
11720  * @protected
11721  */
11722 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
11723         const $input = $( '<input>' ).attr( 'type', config.type );
11725         if ( config.type === 'number' ) {
11726                 $input.attr( 'step', 'any' );
11727         }
11729         return $input;
11733  * Get sanitized value for 'type' for given config. Subclasses might support other types.
11735  * @param {Object} config Configuration options
11736  * @param {string} [config.type='text']
11737  * @return {string}
11738  * @protected
11739  */
11740 OO.ui.TextInputWidget.prototype.getValidType = function ( config ) {
11741         const allowedTypes = [
11742                 'text',
11743                 'password',
11744                 'email',
11745                 'url',
11746                 'number'
11747         ];
11748         return allowedTypes.indexOf( config.type ) !== -1 ? config.type : 'text';
11752  * Focus the input and select a specified range within the text.
11754  * @param {number} from Select from offset
11755  * @param {number} [to=from] Select to offset
11756  * @chainable
11757  * @return {OO.ui.Widget} The widget, for chaining
11758  */
11759 OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
11760         const input = this.$input[ 0 ];
11762         to = to || from;
11764         const isBackwards = to < from,
11765                 start = isBackwards ? to : from,
11766                 end = isBackwards ? from : to;
11768         this.focus();
11770         try {
11771                 input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
11772         } catch ( e ) {
11773                 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
11774                 // Rather than expensively check if the input is attached every time, just check
11775                 // if it was the cause of an error being thrown. If not, rethrow the error.
11776                 if ( this.getElementDocument().body.contains( input ) ) {
11777                         throw e;
11778                 }
11779         }
11780         return this;
11784  * Get an object describing the current selection range in a directional manner
11786  * @return {Object} Object containing 'from' and 'to' offsets
11787  */
11788 OO.ui.TextInputWidget.prototype.getRange = function () {
11789         const input = this.$input[ 0 ],
11790                 start = input.selectionStart,
11791                 end = input.selectionEnd,
11792                 isBackwards = input.selectionDirection === 'backward';
11794         return {
11795                 from: isBackwards ? end : start,
11796                 to: isBackwards ? start : end
11797         };
11801  * Get the length of the text input value.
11803  * This could differ from the length of #getValue if the
11804  * value gets filtered
11806  * @return {number} Input length
11807  */
11808 OO.ui.TextInputWidget.prototype.getInputLength = function () {
11809         return this.$input[ 0 ].value.length;
11813  * Focus the input and select the entire text.
11815  * @chainable
11816  * @return {OO.ui.Widget} The widget, for chaining
11817  */
11818 OO.ui.TextInputWidget.prototype.select = function () {
11819         return this.selectRange( 0, this.getInputLength() );
11823  * Focus the input and move the cursor to the start.
11825  * @chainable
11826  * @return {OO.ui.Widget} The widget, for chaining
11827  */
11828 OO.ui.TextInputWidget.prototype.moveCursorToStart = function () {
11829         return this.selectRange( 0 );
11833  * Focus the input and move the cursor to the end.
11835  * @chainable
11836  * @return {OO.ui.Widget} The widget, for chaining
11837  */
11838 OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
11839         return this.selectRange( this.getInputLength() );
11843  * Insert new content into the input.
11845  * @param {string} content Content to be inserted
11846  * @chainable
11847  * @return {OO.ui.Widget} The widget, for chaining
11848  */
11849 OO.ui.TextInputWidget.prototype.insertContent = function ( content ) {
11850         const value = this.getValue(),
11851                 range = this.getRange(),
11852                 start = Math.min( range.from, range.to ),
11853                 end = Math.max( range.from, range.to );
11855         this.setValue( value.slice( 0, start ) + content + value.slice( end ) );
11856         this.selectRange( start + content.length );
11857         return this;
11861  * Insert new content either side of a selection.
11863  * @param {string} pre Content to be inserted before the selection
11864  * @param {string} post Content to be inserted after the selection
11865  * @chainable
11866  * @return {OO.ui.Widget} The widget, for chaining
11867  */
11868 OO.ui.TextInputWidget.prototype.encapsulateContent = function ( pre, post ) {
11869         const offset = pre.length,
11870                 range = this.getRange(),
11871                 start = Math.min( range.from, range.to ),
11872                 end = Math.max( range.from, range.to );
11874         this.selectRange( start ).insertContent( pre );
11875         this.selectRange( offset + end ).insertContent( post );
11877         this.selectRange( offset + start, offset + end );
11878         return this;
11882  * Set the validation pattern.
11884  * The validation pattern is either a regular expression, a function, or the symbolic name of a
11885  * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11886  * value must contain only numbers).
11888  * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
11889  *  of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11890  */
11891 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
11892         this.validate = validate instanceof RegExp || validate instanceof Function ?
11893                 validate :
11894                 this.constructor.static.validationPatterns[ validate ];
11898  * Sets the 'invalid' flag appropriately.
11900  * @param {boolean} [isValid] Optionally override validation result
11901  */
11902 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
11903         const setFlag = ( valid ) => {
11904                 if ( !valid ) {
11905                         this.$input.attr( 'aria-invalid', 'true' );
11906                 } else {
11907                         this.$input.removeAttr( 'aria-invalid' );
11908                 }
11909                 this.setFlags( { invalid: !valid } );
11910         };
11912         if ( isValid !== undefined ) {
11913                 setFlag( isValid );
11914         } else {
11915                 this.getValidity().then( () => {
11916                         setFlag( true );
11917                 }, () => {
11918                         setFlag( false );
11919                 } );
11920         }
11924  * Get the validity of current value.
11926  * This method returns a promise that resolves if the value is valid and rejects if
11927  * it isn't. Uses the {@link OO.ui.TextInputWidget#validate validation pattern}  to check for validity.
11929  * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
11930  */
11931 OO.ui.TextInputWidget.prototype.getValidity = function () {
11932         function rejectOrResolve( valid ) {
11933                 const deferred = $.Deferred(),
11934                         promise = valid ? deferred.resolve() : deferred.reject();
11935                 return promise.promise();
11936         }
11938         // Check browser validity and reject if it is invalid
11939         if ( this.$input[ 0 ].checkValidity && this.$input[ 0 ].checkValidity() === false ) {
11940                 return rejectOrResolve( false );
11941         }
11943         if ( !this.validate ) {
11944                 return rejectOrResolve( true );
11945         }
11947         // Run our checks if the browser thinks the field is valid
11948         let result;
11949         if ( this.validate instanceof Function ) {
11950                 result = this.validate( this.getValue() );
11951                 if ( result && typeof result.promise === 'function' ) {
11952                         return result.promise().then( ( valid ) => rejectOrResolve( valid ) );
11953                 }
11954         } else {
11955                 // The only other type we accept is a RegExp, see #setValidation
11956                 result = this.validate.test( this.getValue() );
11957         }
11958         return rejectOrResolve( result );
11962  * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11964  * @param {string} labelPosition Label position, 'before' or 'after'
11965  * @chainable
11966  * @return {OO.ui.Widget} The widget, for chaining
11967  */
11968 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
11969         this.labelPosition = labelPosition;
11970         if ( this.label ) {
11971                 // If there is no label and we only change the position, #updatePosition is a no-op,
11972                 // but it takes really a lot of work to do nothing.
11973                 this.updatePosition();
11974         }
11975         return this;
11979  * Update the position of the inline label.
11981  * This method is called by #setLabelPosition, and can also be called on its own if
11982  * something causes the label to be mispositioned.
11984  * @chainable
11985  * @return {OO.ui.Widget} The widget, for chaining
11986  */
11987 OO.ui.TextInputWidget.prototype.updatePosition = function () {
11988         const after = this.labelPosition === 'after';
11990         this.$element
11991                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
11992                 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
11994         this.valCache = null;
11995         this.scrollWidth = null;
11996         this.positionLabel();
11998         return this;
12002  * Position the label by setting the correct padding on the input.
12004  * @private
12005  * @chainable
12006  * @return {OO.ui.Widget} The widget, for chaining
12007  */
12008 OO.ui.TextInputWidget.prototype.positionLabel = function () {
12009         if ( this.isWaitingToBeAttached ) {
12010                 // #onElementAttach will be called soon, which calls this method
12011                 return this;
12012         }
12014         const newCss = {
12015                 'padding-right': '',
12016                 'padding-left': ''
12017         };
12019         if ( this.label ) {
12020                 this.$element.append( this.$label );
12021         } else {
12022                 this.$label.detach();
12023                 // Clear old values if present
12024                 this.$input.css( newCss );
12025                 return;
12026         }
12028         const after = this.labelPosition === 'after',
12029                 rtl = this.$element.css( 'direction' ) === 'rtl',
12030                 property = after === rtl ? 'padding-left' : 'padding-right';
12032         newCss[ property ] = this.$label.outerWidth( true ) + ( after ? this.scrollWidth : 0 );
12033         // We have to clear the padding on the other side, in case the element direction changed
12034         this.$input.css( newCss );
12036         return this;
12040  * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
12041  * {@link OO.ui.mixin.IconElement 'search' icon} as well as a functional
12042  * {@link OO.ui.mixin.IndicatorElement 'clear' indicator} by default.
12043  * Please see the [OOUI documentation on MediaWiki][1] for more information and examples.
12045  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
12047  * @class
12048  * @extends OO.ui.TextInputWidget
12050  * @constructor
12051  * @param {Object} [config] Configuration options
12052  */
12053 OO.ui.SearchInputWidget = function OoUiSearchInputWidget( config ) {
12054         config = Object.assign( {
12055                 icon: 'search'
12056         }, config );
12058         // Parent constructor
12059         OO.ui.SearchInputWidget.super.call( this, config );
12061         // Events
12062         this.connect( this, {
12063                 change: 'onChange'
12064         } );
12065         this.$indicator.on( 'click', this.onIndicatorClick.bind( this ) );
12066         this.$indicator.on( 'keydown', this.onIndicatorKeyDown.bind( this ) );
12068         // Initialization
12069         this.updateSearchIndicator();
12070         this.connect( this, {
12071                 disable: 'onDisable'
12072         } );
12073         this.$indicator
12074                 .attr( {
12075                         tabindex: -1,
12076                         role: 'button'
12077                 } );
12080 /* Setup */
12082 OO.inheritClass( OO.ui.SearchInputWidget, OO.ui.TextInputWidget );
12084 /* Methods */
12087  * @inheritdoc
12088  * @protected
12089  */
12090 OO.ui.SearchInputWidget.prototype.getValidType = function () {
12091         return 'search';
12095  * Clear and focus the input element when pressing enter on the 'clear' indicator.
12097  * @param {jQuery.Event} e KeyDown event
12098  * @return {boolean}
12099  */
12100 OO.ui.SearchInputWidget.prototype.onIndicatorKeyDown = function ( e ) {
12101         if ( e.keyCode === OO.ui.Keys.ENTER ) {
12102                 // Clear the text field
12103                 this.setValue( '' );
12104                 this.focus();
12105                 return false;
12106         }
12110  * Clear and focus the input element when clicking on the 'clear' indicator.
12112  * @param {jQuery.Event} e Click event
12113  * @return {boolean}
12114  */
12115 OO.ui.SearchInputWidget.prototype.onIndicatorClick = function ( e ) {
12116         if ( e.which === OO.ui.MouseButtons.LEFT ) {
12117                 // Clear the text field
12118                 this.setValue( '' );
12119                 this.focus();
12120                 return false;
12121         }
12125  * Update the 'clear' indicator displayed on type: 'search' text
12126  * fields, hiding it when the field is already empty or when it's not
12127  * editable.
12128  */
12129 OO.ui.SearchInputWidget.prototype.updateSearchIndicator = function () {
12130         if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
12131                 this.setIndicator( null );
12132         } else {
12133                 this.setIndicator( 'clear' );
12134                 this.$indicator.attr( 'aria-label', OO.ui.msg( 'ooui-item-remove' ) );
12135         }
12139  * Handle change events.
12141  * @private
12142  */
12143 OO.ui.SearchInputWidget.prototype.onChange = function () {
12144         this.updateSearchIndicator();
12148  * Handle disable events.
12150  * @param {boolean} disabled Element is disabled
12151  * @private
12152  */
12153 OO.ui.SearchInputWidget.prototype.onDisable = function () {
12154         this.updateSearchIndicator();
12158  * @inheritdoc
12159  */
12160 OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) {
12161         OO.ui.SearchInputWidget.super.prototype.setReadOnly.call( this, state );
12162         this.updateSearchIndicator();
12163         return this;
12167  * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
12168  * configure number of rows visible. In addition, these widgets can be autosized to fit user
12169  * inputs and can show {@link OO.ui.mixin.IconElement icons} and
12170  * {@link OO.ui.mixin.IndicatorElement indicators}.
12171  * Please see the [OOUI documentation on MediaWiki][1] for more information and examples.
12173  * MultilineTextInputWidgets can also be used when a single line string is required, but
12174  * we want to display it to the user over mulitple lines (wrapped). This is done by setting
12175  * the `allowLinebreaks` config to `false`.
12177  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
12179  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
12181  *     @example
12182  *     // A MultilineTextInputWidget.
12183  *     const multilineTextInput = new OO.ui.MultilineTextInputWidget( {
12184  *         value: 'Text input on multiple lines'
12185  *     } );
12186  *     $( document.body ).append( multilineTextInput.$element );
12188  * @class
12189  * @extends OO.ui.TextInputWidget
12191  * @constructor
12192  * @param {Object} [config] Configuration options
12193  * @param {number} [config.rows] Number of visible lines in textarea. If used with `autosize`,
12194  *  specifies minimum number of rows to display.
12195  * @param {boolean} [config.autosize=false] Automatically resize the text input to fit its content.
12196  *  Use the #maxRows config to specify a maximum number of displayed rows.
12197  * @param {number} [config.maxRows] Maximum number of rows to display when #autosize is set to true.
12198  *  Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
12199  * @param {boolean} [config.allowLinebreaks=true] Whether to allow the user to add line breaks.
12200  */
12201 OO.ui.MultilineTextInputWidget = function OoUiMultilineTextInputWidget( config ) {
12202         config = Object.assign( {
12203                 type: 'text'
12204         }, config );
12206         // This property needs to exist before setValue in the parent constructor,
12207         // otherwise any linebreaks in the initial value won't be stripped by
12208         // cleanUpValue:
12209         this.allowLinebreaks = config.allowLinebreaks !== undefined ? config.allowLinebreaks : true;
12211         // Parent constructor
12212         OO.ui.MultilineTextInputWidget.super.call( this, config );
12214         // Properties
12215         this.autosize = !!config.autosize;
12216         this.styleHeight = null;
12217         this.minRows = config.rows !== undefined ? config.rows : '';
12218         this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
12220         // Clone for resizing
12221         if ( this.autosize ) {
12222                 this.$clone = this.$input
12223                         .clone()
12224                         .removeAttr( 'id' )
12225                         .removeAttr( 'name' )
12226                         .insertAfter( this.$input )
12227                         .attr( 'aria-hidden', 'true' )
12228                         // Exclude scrollbars when calculating new size (T297963)
12229                         .css( 'overflow', 'hidden' )
12230                         .addClass( 'oo-ui-element-hidden' );
12231         }
12233         // Events
12234         this.connect( this, {
12235                 change: 'onChange'
12236         } );
12238         // Initialization
12239         if ( config.rows ) {
12240                 this.$input.attr( 'rows', config.rows );
12241         }
12242         if ( this.autosize ) {
12243                 this.$input.addClass( 'oo-ui-textInputWidget-autosized' );
12244                 this.isWaitingToBeAttached = true;
12245                 this.installParentChangeDetector();
12246         }
12249 /* Setup */
12251 OO.inheritClass( OO.ui.MultilineTextInputWidget, OO.ui.TextInputWidget );
12253 /* Events */
12256  * An `resize` event is emitted when the widget changes size via the autosize functionality.
12258  * @event OO.ui.MultilineTextInputWidget#resize
12259  */
12261 /* Static Methods */
12264  * @inheritdoc
12265  */
12266 OO.ui.MultilineTextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
12267         const state = OO.ui.MultilineTextInputWidget.super.static.gatherPreInfuseState( node, config );
12268         if ( config.$input ) {
12269                 state.scrollTop = config.$input.scrollTop();
12270         }
12271         return state;
12274 /* Methods */
12277  * @inheritdoc
12278  */
12279 OO.ui.MultilineTextInputWidget.prototype.onElementAttach = function () {
12280         OO.ui.MultilineTextInputWidget.super.prototype.onElementAttach.call( this );
12281         this.adjustSize();
12285  * Handle change events.
12287  * @private
12288  */
12289 OO.ui.MultilineTextInputWidget.prototype.onChange = function () {
12290         this.adjustSize();
12294  * @inheritdoc
12295  */
12296 OO.ui.MultilineTextInputWidget.prototype.updatePosition = function () {
12297         OO.ui.MultilineTextInputWidget.super.prototype.updatePosition.call( this );
12298         this.adjustSize();
12302  * @inheritdoc
12304  * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
12305  */
12306 OO.ui.MultilineTextInputWidget.prototype.onKeyPress = function ( e ) {
12307         if ( !this.allowLinebreaks ) {
12308                 // In this mode we're pretending to be a single-line input, so we
12309                 // prevent adding newlines and react to enter in the same way as
12310                 // TextInputWidget:
12311                 if ( e.which === OO.ui.Keys.ENTER ) {
12312                         e.preventDefault();
12313                 }
12314                 return OO.ui.TextInputWidget.prototype.onKeyPress.call( this, e );
12315         }
12316         if (
12317                 ( e.which === OO.ui.Keys.ENTER && ( e.ctrlKey || e.metaKey ) ) ||
12318                 // Some platforms emit keycode 10 for Control+Enter keypress in a textarea
12319                 e.which === 10
12320         ) {
12321                 this.emit( 'enter', e );
12322         }
12326  * @inheritdoc
12327  */
12328 OO.ui.MultilineTextInputWidget.prototype.cleanUpValue = function ( value ) {
12329         // Parent method will guarantee we're dealing with a string, and apply inputFilter:
12330         value = OO.ui.MultilineTextInputWidget.super.prototype.cleanUpValue( value );
12331         if ( !this.allowLinebreaks ) {
12332                 // If we're forbidding linebreaks then clean them out of the incoming
12333                 // value to avoid a confusing situation
12334                 // TODO: Better handle a paste with linebreaks by using the paste event, as when
12335                 // we use input filtering the cursor is always reset to the end of the input.
12336                 value = value.replace( /\r?\n/g, ' ' );
12337         }
12338         return value;
12342  * Automatically adjust the size of the text input.
12344  * This only affects multiline inputs that are {@link OO.ui.MultilineTextInputWidget#autosize autosized}.
12346  * @chainable
12347  * @param {boolean} [force=false] Force an update, even if the value hasn't changed
12348  * @return {OO.ui.Widget} The widget, for chaining
12349  * @fires OO.ui.MultilineTextInputWidget#resize
12350  */
12351 OO.ui.MultilineTextInputWidget.prototype.adjustSize = function ( force ) {
12352         if ( force || this.$input.val() !== this.valCache ) {
12353                 if ( this.autosize ) {
12354                         this.$clone
12355                                 .val( this.$input.val() )
12356                                 .attr( 'rows', this.minRows )
12357                                 // Set inline height property to 0 to measure scroll height
12358                                 .css( 'height', 0 )
12359                                 .removeClass( 'oo-ui-element-hidden' );
12361                         this.valCache = this.$input.val();
12363                         // https://bugzilla.mozilla.org/show_bug.cgi?id=1799404
12364                         // eslint-disable-next-line no-unused-expressions
12365                         this.$clone[ 0 ].scrollHeight;
12366                         const scrollHeight = this.$clone[ 0 ].scrollHeight;
12368                         // Remove inline height property to measure natural heights
12369                         this.$clone.css( 'height', '' );
12370                         const innerHeight = this.$clone.innerHeight();
12371                         const outerHeight = this.$clone.outerHeight();
12373                         // Measure max rows height
12374                         this.$clone
12375                                 .attr( 'rows', this.maxRows )
12376                                 .css( 'height', 'auto' )
12377                                 .val( '' );
12378                         const maxInnerHeight = this.$clone.innerHeight();
12380                         // Difference between reported innerHeight and scrollHeight with no scrollbars present.
12381                         // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
12382                         const measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
12383                         const idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
12385                         this.$clone.addClass( 'oo-ui-element-hidden' );
12387                         // Only apply inline height when expansion beyond natural height is needed
12388                         // Use the difference between the inner and outer height as a buffer
12389                         const newHeight = idealHeight > innerHeight ? idealHeight + ( outerHeight - innerHeight ) : '';
12390                         if ( newHeight !== this.styleHeight ) {
12391                                 this.$input.css( 'height', newHeight );
12392                                 this.styleHeight = newHeight;
12393                                 this.emit( 'resize' );
12394                         }
12395                 }
12396                 const scrollWidth = this.$input[ 0 ].offsetWidth - this.$input[ 0 ].clientWidth;
12397                 if ( scrollWidth !== this.scrollWidth ) {
12398                         const property = this.$element.css( 'direction' ) === 'rtl' ? 'left' : 'right';
12399                         // Reset
12400                         this.$label.css( { right: '', left: '' } );
12401                         this.$indicator.css( { right: '', left: '' } );
12403                         if ( scrollWidth ) {
12404                                 this.$indicator.css( property, scrollWidth );
12405                                 if ( this.labelPosition === 'after' ) {
12406                                         this.$label.css( property, scrollWidth );
12407                                 }
12408                         }
12410                         this.scrollWidth = scrollWidth;
12411                         this.positionLabel();
12412                 }
12413         }
12414         return this;
12418  * @inheritdoc
12419  * @protected
12420  */
12421 OO.ui.MultilineTextInputWidget.prototype.getInputElement = function () {
12422         return $( '<textarea>' );
12426  * Check if the input automatically adjusts its size.
12428  * @return {boolean}
12429  */
12430 OO.ui.MultilineTextInputWidget.prototype.isAutosizing = function () {
12431         return !!this.autosize;
12435  * @inheritdoc
12436  */
12437 OO.ui.MultilineTextInputWidget.prototype.restorePreInfuseState = function ( state ) {
12438         OO.ui.MultilineTextInputWidget.super.prototype.restorePreInfuseState.call( this, state );
12439         if ( state.scrollTop !== undefined ) {
12440                 this.$input.scrollTop( state.scrollTop );
12441         }
12445  * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12446  * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
12447  * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
12449  * - by typing a value in the text input field. If the value exactly matches the value of a menu
12450  *   option, that option will appear to be selected.
12451  * - by choosing a value from the menu. The value of the chosen option will then appear in the text
12452  *   input field.
12454  * After the user chooses an option, its `data` will be used as a new value for the widget.
12455  * A `label` also can be specified for each option: if given, it will be shown instead of the
12456  * `data` in the dropdown menu.
12458  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
12460  * For more information about menus and options, please see the
12461  * [OOUI documentation on MediaWiki][1].
12463  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
12465  *     @example
12466  *     // A ComboBoxInputWidget.
12467  *     const comboBox = new OO.ui.ComboBoxInputWidget( {
12468  *         value: 'Option 1',
12469  *         options: [
12470  *             { data: 'Option 1' },
12471  *             { data: 'Option 2' },
12472  *             { data: 'Option 3' }
12473  *         ]
12474  *     } );
12475  *     $( document.body ).append( comboBox.$element );
12477  *     @example <caption>A ComboBoxInputWidget can have additional option labels:</caption>
12478  *     const comboBox = new OO.ui.ComboBoxInputWidget( {
12479  *         value: 'Option 1',
12480  *         options: [
12481  *             {
12482  *                 data: 'Option 1',
12483  *                 label: 'Option One'
12484  *             },
12485  *             {
12486  *                 data: 'Option 2',
12487  *                 label: 'Option Two'
12488  *             },
12489  *             {
12490  *                 data: 'Option 3',
12491  *                 label: 'Option Three'
12492  *             }
12493  *         ]
12494  *     } );
12495  *     $( document.body ).append( comboBox.$element );
12497  * @class
12498  * @extends OO.ui.TextInputWidget
12500  * @constructor
12501  * @param {Object} [config] Configuration options
12502  * @param {Object[]} [config.options=[]] Array of menu options in the format `{ data: …, label: … }`
12503  * @param {Object} [config.menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu
12504  *  select widget}.
12505  * @param {jQuery} [config.$overlay] Render the menu into a separate layer. This configuration is useful
12506  *  in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
12507  *  layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
12508  *  uses relative positioning.
12509  *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12510  */
12511 OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
12512         // Configuration initialization
12513         config = Object.assign( {
12514                 autocomplete: false
12515         }, config );
12517         // See InputWidget#reusePreInfuseDOM about `config.$input`
12518         if ( config.$input ) {
12519                 config.$input.removeAttr( 'list' );
12520         }
12522         // Parent constructor
12523         OO.ui.ComboBoxInputWidget.super.call( this, config );
12525         // Properties
12526         this.$overlay = ( config.$overlay === true ?
12527                 OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
12528         this.dropdownButton = new OO.ui.ButtonWidget( {
12529                 classes: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
12530                 label: OO.ui.msg( 'ooui-combobox-button-label' ),
12531                 indicator: 'down',
12532                 invisibleLabel: true,
12533                 disabled: this.disabled
12534         } );
12535         this.menu = new OO.ui.MenuSelectWidget( Object.assign(
12536                 {
12537                         widget: this,
12538                         input: this,
12539                         $floatableContainer: this.$element,
12540                         disabled: this.isDisabled()
12541                 },
12542                 config.menu
12543         ) );
12545         // Events
12546         this.connect( this, {
12547                 change: 'onInputChange',
12548                 enter: 'onInputEnter'
12549         } );
12550         this.dropdownButton.connect( this, {
12551                 click: 'onDropdownButtonClick'
12552         } );
12553         this.menu.connect( this, {
12554                 choose: 'onMenuChoose',
12555                 add: 'onMenuItemsChange',
12556                 remove: 'onMenuItemsChange',
12557                 toggle: 'onMenuToggle'
12558         } );
12560         // Initialization
12561         this.$input.attr( {
12562                 role: 'combobox',
12563                 'aria-owns': this.menu.getElementId(),
12564                 'aria-autocomplete': 'list'
12565         } );
12566         this.dropdownButton.$button.attr( {
12567                 'aria-controls': this.menu.getElementId()
12568         } );
12569         // Do not override options set via config.menu.items
12570         if ( config.options !== undefined ) {
12571                 this.setOptions( config.options );
12572         }
12573         this.$field = $( '<div>' )
12574                 .addClass( 'oo-ui-comboBoxInputWidget-field' )
12575                 .append( this.$input, this.dropdownButton.$element );
12576         this.$element
12577                 .addClass( 'oo-ui-comboBoxInputWidget' )
12578                 .append( this.$field );
12579         this.$overlay.append( this.menu.$element );
12580         this.onMenuItemsChange();
12583 /* Setup */
12585 OO.inheritClass( OO.ui.ComboBoxInputWidget, OO.ui.TextInputWidget );
12587 /* Methods */
12590  * Get the combobox's menu.
12592  * @return {OO.ui.MenuSelectWidget} Menu widget
12593  */
12594 OO.ui.ComboBoxInputWidget.prototype.getMenu = function () {
12595         return this.menu;
12599  * Get the combobox's text input widget.
12601  * @return {OO.ui.TextInputWidget} Text input widget
12602  */
12603 OO.ui.ComboBoxInputWidget.prototype.getInput = function () {
12604         return this;
12608  * @inheritdoc
12609  */
12610 OO.ui.ComboBoxInputWidget.prototype.onEdit = function ( event ) {
12611         // Parent method
12612         OO.ui.ComboBoxInputWidget.super.prototype.onEdit.apply( this, arguments );
12614         if ( this.menu.isVisible() || this.isDisabled() || !this.isVisible() ) {
12615                 return;
12616         }
12618         if ( event.type === 'input' || event.type === 'mouseup' || ( event.type === 'keydown' && (
12619                 event.keyCode === OO.ui.Keys.ENTER ||
12620                 event.keyCode === OO.ui.Keys.UP ||
12621                 event.keyCode === OO.ui.Keys.DOWN
12622         ) ) ) {
12623                 this.menu.toggle( true );
12624         }
12628  * Handle input change events.
12630  * @private
12631  * @param {string} value New value
12632  */
12633 OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
12634         const match = this.menu.findItemFromData( value );
12636         this.menu.selectItem( match );
12637         if ( this.menu.findHighlightedItem() ) {
12638                 this.menu.highlightItem( match );
12639         }
12643  * Handle input enter events.
12645  * @private
12646  */
12647 OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
12648         if ( !this.isDisabled() ) {
12649                 this.menu.toggle( false );
12650         }
12654  * Handle button click events.
12656  * @private
12657  */
12658 OO.ui.ComboBoxInputWidget.prototype.onDropdownButtonClick = function () {
12659         this.menu.toggle();
12660         this.focus();
12664  * Handle menu choose events.
12666  * @private
12667  * @param {OO.ui.OptionWidget} item Chosen item
12668  */
12669 OO.ui.ComboBoxInputWidget.prototype.onMenuChoose = function ( item ) {
12670         this.setValue( item.getData() );
12674  * Handle menu item change events.
12676  * @private
12677  */
12678 OO.ui.ComboBoxInputWidget.prototype.onMenuItemsChange = function () {
12679         const match = this.menu.findItemFromData( this.getValue() );
12680         this.menu.selectItem( match );
12681         if ( this.menu.findHighlightedItem() ) {
12682                 this.menu.highlightItem( match );
12683         }
12684         this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu.isEmpty() );
12688  * Handle menu toggle events.
12690  * @private
12691  * @param {boolean} isVisible Open state of the menu
12692  */
12693 OO.ui.ComboBoxInputWidget.prototype.onMenuToggle = function ( isVisible ) {
12694         this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible );
12698  * Update the disabled state of the controls
12700  * @chainable
12701  * @protected
12702  * @return {OO.ui.ComboBoxInputWidget} The widget, for chaining
12703  */
12704 OO.ui.ComboBoxInputWidget.prototype.updateControlsDisabled = function () {
12705         const disabled = this.isDisabled() || this.isReadOnly();
12706         if ( this.dropdownButton ) {
12707                 this.dropdownButton.setDisabled( disabled );
12708         }
12709         if ( this.menu ) {
12710                 this.menu.setDisabled( disabled );
12711         }
12712         return this;
12716  * @inheritdoc
12717  */
12718 OO.ui.ComboBoxInputWidget.prototype.setDisabled = function () {
12719         // Parent method
12720         OO.ui.ComboBoxInputWidget.super.prototype.setDisabled.apply( this, arguments );
12721         this.updateControlsDisabled();
12722         return this;
12726  * @inheritdoc
12727  */
12728 OO.ui.ComboBoxInputWidget.prototype.setReadOnly = function () {
12729         // Parent method
12730         OO.ui.ComboBoxInputWidget.super.prototype.setReadOnly.apply( this, arguments );
12731         this.updateControlsDisabled();
12732         return this;
12736  * Set the options available for this input.
12738  * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
12739  * @chainable
12740  * @return {OO.ui.Widget} The widget, for chaining
12741  */
12742 OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
12743         this.getMenu()
12744                 .clearItems()
12745                 .addItems( options.map( ( opt ) => new OO.ui.MenuOptionWidget( {
12746                         data: opt.data,
12747                         label: opt.label !== undefined ? opt.label : opt.data
12748                 } ) ) );
12750         return this;
12754  * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
12755  * which is a widget that is specified by reference before any optional configuration settings.
12757  * Field layouts can be configured with help text and/or labels. Labels are aligned in one of
12758  * four ways:
12760  * - **left**: The label is placed before the field-widget and aligned with the left margin.
12761  *   A left-alignment is used for forms with many fields.
12762  * - **right**: The label is placed before the field-widget and aligned to the right margin.
12763  *   A right-alignment is used for long but familiar forms which users tab through,
12764  *   verifying the current field with a quick glance at the label.
12765  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12766  *   that users fill out from top to bottom.
12767  * - **inline**: The label is placed after the field-widget and aligned to the left.
12768  *   An inline-alignment is best used with checkboxes or radio buttons.
12770  * Help text can either be:
12772  * - accessed via a help icon that appears in the upper right corner of the rendered field layout,
12773  *   or
12774  * - shown as a subtle explanation below the label.
12776  * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`.
12777  * If it is long or not essential, leave `helpInline` to its default, `false`.
12779  * Please see the [OOUI documentation on MediaWiki][1] for examples and more information.
12781  * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12783  * @class
12784  * @extends OO.ui.Layout
12785  * @mixes OO.ui.mixin.LabelElement
12786  * @mixes OO.ui.mixin.TitledElement
12788  * @constructor
12789  * @param {OO.ui.Widget} fieldWidget Field widget
12790  * @param {Object} [config] Configuration options
12791  * @param {string} [config.align='left'] Alignment of the label: 'left', 'right', 'top'
12792  *  or 'inline'
12793  * @param {Array} [config.errors] Error messages about the widget, which will be
12794  *  displayed below the widget.
12795  * @param {Array} [config.warnings] Warning messages about the widget, which will be
12796  *  displayed below the widget.
12797  * @param {Array} [config.successMessages] Success messages on user interactions with the widget,
12798  *  which will be displayed below the widget.
12799  *  The array may contain strings or OO.ui.HtmlSnippet instances.
12800  * @param {Array} [config.notices] Notices about the widget, which will be displayed
12801  *  below the widget.
12802  *  The array may contain strings or OO.ui.HtmlSnippet instances.
12803  *  These are more visible than `help` messages when `helpInline` is set, and so
12804  *  might be good for transient messages.
12805  * @param {string|OO.ui.HtmlSnippet} [config.help] Help text. When help text is specified
12806  *  and `helpInline` is `false`, a "help" icon will appear in the upper-right
12807  *  corner of the rendered field; clicking it will display the text in a popup.
12808  *  If `helpInline` is `true`, then a subtle description will be shown after the
12809  *  label.
12810  * @param {boolean} [config.helpInline=false] Whether or not the help should be inline,
12811  *  or shown when the "help" icon is clicked.
12812  * @param {jQuery} [config.$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
12813  * `help` is given.
12814  *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12816  * @throws {Error} An error is thrown if no widget is specified
12818  * @property {OO.ui.Widget} fieldWidget
12819  */
12820 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
12821         // Allow passing positional parameters inside the config object
12822         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
12823                 config = fieldWidget;
12824                 fieldWidget = config.fieldWidget;
12825         }
12827         // Make sure we have required constructor arguments
12828         if ( fieldWidget === undefined ) {
12829                 throw new Error( 'Widget not found' );
12830         }
12832         // Configuration initialization
12833         config = Object.assign( { align: 'left', helpInline: false }, config );
12835         if ( config.help && !config.label ) {
12836                 // Add an empty label. For some combinations of 'helpInline' and 'align'
12837                 // there would be no space in the interface to display the help text otherwise.
12838                 config.label = ' ';
12839         }
12841         // Parent constructor
12842         OO.ui.FieldLayout.super.call( this, config );
12844         // Mixin constructors
12845         OO.ui.mixin.LabelElement.call( this, Object.assign( {
12846                 $label: $( '<label>' )
12847         }, config ) );
12848         OO.ui.mixin.TitledElement.call( this, Object.assign( { $titled: this.$label }, config ) );
12850         // Properties
12851         this.fieldWidget = fieldWidget;
12852         this.errors = [];
12853         this.warnings = [];
12854         this.successMessages = [];
12855         this.notices = [];
12856         this.$field = this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12857         this.$messages = $( '<div>' );
12858         this.$header = $( '<span>' );
12859         this.$body = $( '<div>' );
12860         this.align = null;
12861         this.helpInline = config.helpInline;
12863         // Events
12864         this.fieldWidget.connect( this, {
12865                 disable: 'onFieldDisable'
12866         } );
12868         // Initialization
12869         this.$help = config.help ?
12870                 this.createHelpElement( config.help, config.$overlay ) :
12871                 $( [] );
12872         if ( this.fieldWidget.getInputId() ) {
12873                 this.$label.attr( 'for', this.fieldWidget.getInputId() );
12874                 if ( this.helpInline ) {
12875                         this.$help.attr( 'for', this.fieldWidget.getInputId() );
12876                 }
12877         } else {
12878                 // We can't use `label for` with non-form elements, use `aria-labelledby` instead
12879                 const id = OO.ui.generateElementId();
12880                 this.$label.attr( 'id', id );
12881                 this.fieldWidget.setLabelledBy( id );
12883                 // Forward clicks on the label to the widget, like `label for` would do
12884                 this.$label.on( 'click', this.onLabelClick.bind( this ) );
12885                 if ( this.helpInline ) {
12886                         this.$help.on( 'click', this.onLabelClick.bind( this ) );
12887                 }
12888         }
12889         this.$element
12890                 .addClass( 'oo-ui-fieldLayout' )
12891                 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget.isDisabled() )
12892                 .append( this.$body );
12893         this.$body.addClass( 'oo-ui-fieldLayout-body' );
12894         this.$header.addClass( 'oo-ui-fieldLayout-header' );
12895         this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
12896         this.$field
12897                 .addClass( 'oo-ui-fieldLayout-field' )
12898                 .append( this.fieldWidget.$element );
12900         this.setErrors( config.errors || [] );
12901         this.setWarnings( config.warnings || [] );
12902         this.setSuccess( config.successMessages || [] );
12903         this.setNotices( config.notices || [] );
12904         this.setAlignment( config.align );
12905         // Call this again to take into account the widget's accessKey
12906         this.updateTitle();
12909 /* Setup */
12911 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
12912 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
12913 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.TitledElement );
12915 /* Methods */
12918  * Handle field disable events.
12920  * @private
12921  * @param {boolean} value Field is disabled
12922  */
12923 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
12924         this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
12928  * Handle click events on the field label, or inline help
12930  * @param {jQuery.Event} event
12931  */
12932 OO.ui.FieldLayout.prototype.onLabelClick = function () {
12933         this.fieldWidget.simulateLabelClick();
12937  * Get the widget contained by the field.
12939  * @return {OO.ui.Widget} Field widget
12940  */
12941 OO.ui.FieldLayout.prototype.getField = function () {
12942         return this.fieldWidget;
12946  * Return `true` if the given field widget can be used with `'inline'` alignment (see
12947  * #setAlignment). Return `false` if it can't or if this can't be determined.
12949  * @return {boolean}
12950  */
12951 OO.ui.FieldLayout.prototype.isFieldInline = function () {
12952         // This is very simplistic, but should be good enough.
12953         return this.getField().$element.prop( 'tagName' ).toLowerCase() === 'span';
12957  * @protected
12958  * @param {string} kind 'error' or 'notice'
12959  * @param {string|OO.ui.HtmlSnippet} text
12960  * @return {jQuery}
12961  */
12962 OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
12963         return new OO.ui.MessageWidget( {
12964                 type: kind,
12965                 inline: true,
12966                 label: text
12967         } ).$element;
12971  * Set the field alignment mode.
12973  * @private
12974  * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
12975  * @chainable
12976  * @return {OO.ui.BookletLayout} The layout, for chaining
12977  */
12978 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
12979         if ( value !== this.align ) {
12980                 // Default to 'left'
12981                 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
12982                         value = 'left';
12983                 }
12984                 // Validate
12985                 if ( value === 'inline' && !this.isFieldInline() ) {
12986                         value = 'top';
12987                 }
12988                 // Reorder elements
12990                 if ( this.helpInline ) {
12991                         if ( value === 'top' ) {
12992                                 this.$header.append( this.$label );
12993                                 this.$body.append( this.$header, this.$field, this.$help );
12994                         } else if ( value === 'inline' ) {
12995                                 this.$header.append( this.$label, this.$help );
12996                                 this.$body.append( this.$field, this.$header );
12997                         } else {
12998                                 this.$header.append( this.$label, this.$help );
12999                                 this.$body.append( this.$header, this.$field );
13000                         }
13001                 } else {
13002                         if ( value === 'top' ) {
13003                                 this.$header.append( this.$help, this.$label );
13004                                 this.$body.append( this.$header, this.$field );
13005                         } else if ( value === 'inline' ) {
13006                                 this.$header.append( this.$help, this.$label );
13007                                 this.$body.append( this.$field, this.$header );
13008                         } else {
13009                                 this.$header.append( this.$label );
13010                                 this.$body.append( this.$header, this.$help, this.$field );
13011                         }
13012                 }
13013                 // Set classes. The following classes can be used here:
13014                 // * oo-ui-fieldLayout-align-left
13015                 // * oo-ui-fieldLayout-align-right
13016                 // * oo-ui-fieldLayout-align-top
13017                 // * oo-ui-fieldLayout-align-inline
13018                 if ( this.align ) {
13019                         this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
13020                 }
13021                 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
13022                 this.align = value;
13023         }
13025         return this;
13029  * Set the list of error messages.
13031  * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
13032  *  The array may contain strings or OO.ui.HtmlSnippet instances.
13033  * @chainable
13034  * @return {OO.ui.BookletLayout} The layout, for chaining
13035  */
13036 OO.ui.FieldLayout.prototype.setErrors = function ( errors ) {
13037         this.errors = errors.slice();
13038         this.updateMessages();
13039         return this;
13043  * Set the list of warning messages.
13045  * @param {Array} warnings Warning messages about the widget, which will be displayed below
13046  *  the widget.
13047  *  The array may contain strings or OO.ui.HtmlSnippet instances.
13048  * @chainable
13049  * @return {OO.ui.BookletLayout} The layout, for chaining
13050  */
13051 OO.ui.FieldLayout.prototype.setWarnings = function ( warnings ) {
13052         this.warnings = warnings.slice();
13053         this.updateMessages();
13054         return this;
13058  * Set the list of success messages.
13060  * @param {Array} successMessages Success messages about the widget, which will be displayed below
13061  *  the widget.
13062  *  The array may contain strings or OO.ui.HtmlSnippet instances.
13063  * @chainable
13064  * @return {OO.ui.BookletLayout} The layout, for chaining
13065  */
13066 OO.ui.FieldLayout.prototype.setSuccess = function ( successMessages ) {
13067         this.successMessages = successMessages.slice();
13068         this.updateMessages();
13069         return this;
13073  * Set the list of notice messages.
13075  * @param {Array} notices Notices about the widget, which will be displayed below the widget.
13076  *  The array may contain strings or OO.ui.HtmlSnippet instances.
13077  * @chainable
13078  * @return {OO.ui.BookletLayout} The layout, for chaining
13079  */
13080 OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
13081         this.notices = notices.slice();
13082         this.updateMessages();
13083         return this;
13087  * Update the rendering of error, warning, success and notice messages.
13089  * @private
13090  */
13091 OO.ui.FieldLayout.prototype.updateMessages = function () {
13092         this.$messages.empty();
13094         if (
13095                 this.errors.length ||
13096                 this.warnings.length ||
13097                 this.successMessages.length ||
13098                 this.notices.length
13099         ) {
13100                 this.$body.after( this.$messages );
13101         } else {
13102                 this.$messages.remove();
13103                 return;
13104         }
13106         let i;
13107         for ( i = 0; i < this.errors.length; i++ ) {
13108                 this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
13109         }
13110         for ( i = 0; i < this.warnings.length; i++ ) {
13111                 this.$messages.append( this.makeMessage( 'warning', this.warnings[ i ] ) );
13112         }
13113         for ( i = 0; i < this.successMessages.length; i++ ) {
13114                 this.$messages.append( this.makeMessage( 'success', this.successMessages[ i ] ) );
13115         }
13116         for ( i = 0; i < this.notices.length; i++ ) {
13117                 this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
13118         }
13122  * Include information about the widget's accessKey in our title. TitledElement calls this method.
13123  * (This is a bit of a hack.)
13125  * @protected
13126  * @param {string} title Tooltip label for 'title' attribute
13127  * @return {string}
13128  */
13129 OO.ui.FieldLayout.prototype.formatTitleWithAccessKey = function ( title ) {
13130         if ( this.fieldWidget && this.fieldWidget.formatTitleWithAccessKey ) {
13131                 return this.fieldWidget.formatTitleWithAccessKey( title );
13132         }
13133         return title;
13137  * Creates and returns the help element. Also sets the `aria-describedby`
13138  * attribute on the main element of the `fieldWidget`.
13140  * @private
13141  * @param {string|OO.ui.HtmlSnippet} [help] Help text.
13142  * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
13143  * @return {jQuery} The element that should become `this.$help`.
13144  */
13145 OO.ui.FieldLayout.prototype.createHelpElement = function ( help, $overlay ) {
13146         let helpId, helpWidget;
13148         if ( this.helpInline ) {
13149                 helpWidget = new OO.ui.LabelWidget( {
13150                         label: help,
13151                         classes: [ 'oo-ui-inline-help' ]
13152                 } );
13154                 helpId = helpWidget.getElementId();
13155         } else {
13156                 helpWidget = new OO.ui.PopupButtonWidget( {
13157                         $overlay: $overlay,
13158                         popup: {
13159                                 padded: true
13160                         },
13161                         classes: [ 'oo-ui-fieldLayout-help' ],
13162                         framed: false,
13163                         icon: 'info',
13164                         label: OO.ui.msg( 'ooui-field-help' ),
13165                         invisibleLabel: true
13166                 } );
13168                 helpWidget.popup.on( 'ready', () => {
13169                         const $popupElement = helpWidget.popup.$element;
13170                         $popupElement.attr( 'tabindex', 0 );
13171                         $popupElement.trigger( 'focus' );
13172                 } );
13174                 helpWidget.popup.on( 'closing', () => {
13175                         helpWidget.$button.trigger( 'focus' );
13176                 } );
13178                 if ( help instanceof OO.ui.HtmlSnippet ) {
13179                         helpWidget.getPopup().$body.html( help.toString() );
13180                 } else {
13181                         helpWidget.getPopup().$body.text( help );
13182                 }
13184                 helpId = helpWidget.getPopup().getBodyId();
13185         }
13187         // Set the 'aria-describedby' attribute on the fieldWidget
13188         // Preference given to an input or a button
13189         (
13190                 this.fieldWidget.$input ||
13191                 ( this.fieldWidget.input && this.fieldWidget.input.$input ) ||
13192                 this.fieldWidget.$button ||
13193                 this.fieldWidget.$element
13194         ).attr( 'aria-describedby', helpId );
13196         return helpWidget.$element;
13200  * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget,
13201  * a button, and an optional label and/or help text. The field-widget (e.g., a
13202  * {@link OO.ui.TextInputWidget TextInputWidget}), is required and is specified before any optional
13203  * configuration settings.
13205  * Labels can be aligned in one of four ways:
13207  * - **left**: The label is placed before the field-widget and aligned with the left margin.
13208  *   A left-alignment is used for forms with many fields.
13209  * - **right**: The label is placed before the field-widget and aligned to the right margin.
13210  *   A right-alignment is used for long but familiar forms which users tab through,
13211  *   verifying the current field with a quick glance at the label.
13212  * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
13213  *   that users fill out from top to bottom.
13214  * - **inline**: The label is placed after the field-widget and aligned to the left.
13215  *   An inline-alignment is best used with checkboxes or radio buttons.
13217  * Help text is accessed via a help icon that appears in the upper right corner of the rendered
13218  * field layout when help text is specified.
13220  *     @example
13221  *     // Example of an ActionFieldLayout
13222  *     const actionFieldLayout = new OO.ui.ActionFieldLayout(
13223  *         new OO.ui.TextInputWidget( {
13224  *             placeholder: 'Field widget'
13225  *         } ),
13226  *         new OO.ui.ButtonWidget( {
13227  *             label: 'Button'
13228  *         } ),
13229  *         {
13230  *             label: 'An ActionFieldLayout. This label is aligned top',
13231  *             align: 'top',
13232  *             help: 'This is help text'
13233  *         }
13234  *     );
13236  *     $( document.body ).append( actionFieldLayout.$element );
13238  * @class
13239  * @extends OO.ui.FieldLayout
13241  * @constructor
13242  * @param {OO.ui.Widget} fieldWidget Field widget
13243  * @param {OO.ui.ButtonWidget} buttonWidget Button widget
13244  * @param {Object} config
13245  */
13246 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
13247         // Allow passing positional parameters inside the config object
13248         if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
13249                 config = fieldWidget;
13250                 fieldWidget = config.fieldWidget;
13251                 buttonWidget = config.buttonWidget;
13252         }
13254         // Parent constructor
13255         OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
13257         // Properties
13258         this.buttonWidget = buttonWidget;
13259         this.$button = $( '<span>' );
13260         this.$input = this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
13262         // Initialization
13263         this.$element.addClass( 'oo-ui-actionFieldLayout' );
13264         this.$button
13265                 .addClass( 'oo-ui-actionFieldLayout-button' )
13266                 .append( this.buttonWidget.$element );
13267         this.$input
13268                 .addClass( 'oo-ui-actionFieldLayout-input' )
13269                 .append( this.fieldWidget.$element );
13270         this.$field.append( this.$input, this.$button );
13273 /* Setup */
13275 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
13278  * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
13279  * which each contain an individual widget and, optionally, a label. Each Fieldset can be
13280  * configured with a label as well. For more information and examples,
13281  * please see the [OOUI documentation on MediaWiki][1].
13283  *     @example
13284  *     // Example of a fieldset layout
13285  *     const input1 = new OO.ui.TextInputWidget( {
13286  *         placeholder: 'A text input field'
13287  *     } );
13289  *     const input2 = new OO.ui.TextInputWidget( {
13290  *         placeholder: 'A text input field'
13291  *     } );
13293  *     const fieldset = new OO.ui.FieldsetLayout( {
13294  *         label: 'Example of a fieldset layout'
13295  *     } );
13297  *     fieldset.addItems( [
13298  *         new OO.ui.FieldLayout( input1, {
13299  *             label: 'Field One'
13300  *         } ),
13301  *         new OO.ui.FieldLayout( input2, {
13302  *             label: 'Field Two'
13303  *         } )
13304  *     ] );
13305  *     $( document.body ).append( fieldset.$element );
13307  * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
13309  * @class
13310  * @extends OO.ui.Layout
13311  * @mixes OO.ui.mixin.IconElement
13312  * @mixes OO.ui.mixin.LabelElement
13313  * @mixes OO.ui.mixin.GroupElement
13315  * @constructor
13316  * @param {Object} [config] Configuration options
13317  * @param {OO.ui.FieldLayout[]} [config.items] An array of fields to add to the fieldset.
13318  *  See OO.ui.FieldLayout for more information about fields.
13319  * @param {string|OO.ui.HtmlSnippet} [config.help] Help text. When help text is specified
13320  *  and `helpInline` is `false`, a "help" icon will appear in the upper-right
13321  *  corner of the rendered field; clicking it will display the text in a popup.
13322  *  If `helpInline` is `true`, then a subtle description will be shown after the
13323  *  label.
13324  *  For feedback messages, you are advised to use `notices`.
13325  * @param {boolean} [config.helpInline=false] Whether or not the help should be inline,
13326  *  or shown when the "help" icon is clicked.
13327  * @param {jQuery} [config.$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
13328  *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
13329  */
13330 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
13331         // Configuration initialization
13332         config = config || {};
13334         // Parent constructor
13335         OO.ui.FieldsetLayout.super.call( this, config );
13337         // Mixin constructors
13338         OO.ui.mixin.IconElement.call( this, config );
13339         OO.ui.mixin.LabelElement.call( this, config );
13340         OO.ui.mixin.GroupElement.call( this, config );
13342         // Properties
13343         this.$header = $( '<legend>' );
13345         // Initialization
13346         this.$header
13347                 .addClass( 'oo-ui-fieldsetLayout-header' )
13348                 .append( this.$icon, this.$label );
13349         this.$group.addClass( 'oo-ui-fieldsetLayout-group' );
13350         this.$element
13351                 .addClass( 'oo-ui-fieldsetLayout' )
13352                 .prepend( this.$header, this.$group );
13354         // Help
13355         if ( config.help ) {
13356                 if ( config.helpInline ) {
13357                         const inlineHelpWidget = new OO.ui.LabelWidget( {
13358                                 label: config.help,
13359                                 classes: [ 'oo-ui-inline-help' ]
13360                         } );
13361                         this.$element.prepend( this.$header, inlineHelpWidget.$element, this.$group );
13362                 } else {
13363                         const helpWidget = new OO.ui.PopupButtonWidget( {
13364                                 $overlay: config.$overlay,
13365                                 popup: {
13366                                         padded: true
13367                                 },
13368                                 classes: [ 'oo-ui-fieldsetLayout-help' ],
13369                                 framed: false,
13370                                 icon: 'info',
13371                                 label: OO.ui.msg( 'ooui-field-help' ),
13372                                 invisibleLabel: true
13373                         } );
13374                         if ( config.help instanceof OO.ui.HtmlSnippet ) {
13375                                 helpWidget.getPopup().$body.html( config.help.toString() );
13376                         } else {
13377                                 helpWidget.getPopup().$body.text( config.help );
13378                         }
13379                         this.$header.append( helpWidget.$element );
13380                 }
13381         }
13382         this.addItems( config.items || [] );
13385 /* Setup */
13387 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
13388 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
13389 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
13390 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
13392 /* Static Properties */
13395  * @static
13396  * @inheritdoc
13397  */
13398 OO.ui.FieldsetLayout.static.tagName = 'fieldset';
13401  * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use
13402  * browser-based form submission for the fields instead of handling them in JavaScript. Form layouts
13403  * can be configured with an HTML form action, an encoding type, and a method using the #action,
13404  * #enctype, and #method configs, respectively.
13405  * See the [OOUI documentation on MediaWiki][1] for more information and examples.
13407  * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
13408  * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
13409  * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
13410  * some fancier controls. Some controls have both regular and InputWidget variants, for example
13411  * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
13412  * often have simplified APIs to match the capabilities of HTML forms.
13413  * See the [OOUI documentation on MediaWiki][2] for more information about InputWidgets.
13415  * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
13416  * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
13418  *     @example
13419  *     // Example of a form layout that wraps a fieldset layout.
13420  *     const input1 = new OO.ui.TextInputWidget( {
13421  *             placeholder: 'Username'
13422  *         } ),
13423  *         input2 = new OO.ui.TextInputWidget( {
13424  *             placeholder: 'Password',
13425  *             type: 'password'
13426  *         } ),
13427  *         submit = new OO.ui.ButtonInputWidget( {
13428  *             label: 'Submit'
13429  *         } ),
13430  *         fieldset = new OO.ui.FieldsetLayout( {
13431  *             label: 'A form layout'
13432  *         } );
13434  *     fieldset.addItems( [
13435  *         new OO.ui.FieldLayout( input1, {
13436  *             label: 'Username',
13437  *             align: 'top'
13438  *         } ),
13439  *         new OO.ui.FieldLayout( input2, {
13440  *             label: 'Password',
13441  *             align: 'top'
13442  *         } ),
13443  *         new OO.ui.FieldLayout( submit )
13444  *     ] );
13445  *     const form = new OO.ui.FormLayout( {
13446  *         items: [ fieldset ],
13447  *         action: '/api/formhandler',
13448  *         method: 'get'
13449  *     } )
13450  *     $( document.body ).append( form.$element );
13452  * @class
13453  * @extends OO.ui.Layout
13454  * @mixes OO.ui.mixin.GroupElement
13456  * @constructor
13457  * @param {Object} [config] Configuration options
13458  * @param {string} [config.method] HTML form `method` attribute
13459  * @param {string} [config.action] HTML form `action` attribute
13460  * @param {string} [config.enctype] HTML form `enctype` attribute
13461  * @param {OO.ui.FieldsetLayout[]} [config.items] Fieldset layouts to add to the form layout.
13462  */
13463 OO.ui.FormLayout = function OoUiFormLayout( config ) {
13464         // Configuration initialization
13465         config = config || {};
13467         // Parent constructor
13468         OO.ui.FormLayout.super.call( this, config );
13470         // Mixin constructors
13471         OO.ui.mixin.GroupElement.call( this, Object.assign( { $group: this.$element }, config ) );
13473         // Events
13474         this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
13476         // Make sure the action is safe
13477         let action = config.action;
13478         if ( action !== undefined && !OO.ui.isSafeUrl( action ) ) {
13479                 action = './' + action;
13480         }
13482         // Initialization
13483         this.$element
13484                 .addClass( 'oo-ui-formLayout' )
13485                 .attr( {
13486                         method: config.method,
13487                         action: action,
13488                         enctype: config.enctype
13489                 } );
13490         this.addItems( config.items || [] );
13493 /* Setup */
13495 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
13496 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
13498 /* Events */
13501  * A 'submit' event is emitted when the form is submitted.
13503  * @event OO.ui.FormLayout#submit
13504  */
13506 /* Static Properties */
13509  * @static
13510  * @inheritdoc
13511  */
13512 OO.ui.FormLayout.static.tagName = 'form';
13514 /* Methods */
13517  * Handle form submit events.
13519  * @private
13520  * @param {jQuery.Event} e Submit event
13521  * @fires OO.ui.FormLayout#submit
13522  * @return {OO.ui.FormLayout} The layout, for chaining
13523  */
13524 OO.ui.FormLayout.prototype.onFormSubmit = function () {
13525         if ( this.emit( 'submit' ) ) {
13526                 return false;
13527         }
13531  * PanelLayouts expand to cover the entire area of their parent. They can be configured with
13532  * scrolling, padding, and a frame, and are often used together with
13533  * {@link OO.ui.StackLayout StackLayouts}.
13535  *     @example
13536  *     // Example of a panel layout
13537  *     const panel = new OO.ui.PanelLayout( {
13538  *         expanded: false,
13539  *         framed: true,
13540  *         padded: true,
13541  *         $content: $( '<p>A panel layout with padding and a frame.</p>' )
13542  *     } );
13543  *     $( document.body ).append( panel.$element );
13545  * @class
13546  * @extends OO.ui.Layout
13548  * @constructor
13549  * @param {Object} [config] Configuration options
13550  * @param {boolean} [config.scrollable=false] Allow vertical scrolling
13551  * @param {boolean} [config.padded=false] Add padding between the content and the edges of the panel.
13552  * @param {boolean} [config.expanded=true] Expand the panel to fill the entire parent element.
13553  * @param {boolean} [config.framed=false] Render the panel with a frame to visually separate it from outside
13554  *  content.
13555  */
13556 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
13557         // Configuration initialization
13558         config = Object.assign( {
13559                 scrollable: false,
13560                 padded: false,
13561                 expanded: true,
13562                 framed: false
13563         }, config );
13565         // Parent constructor
13566         OO.ui.PanelLayout.super.call( this, config );
13568         // Initialization
13569         this.$element.addClass( 'oo-ui-panelLayout' );
13570         if ( config.scrollable ) {
13571                 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
13572         }
13573         if ( config.padded ) {
13574                 this.$element.addClass( 'oo-ui-panelLayout-padded' );
13575         }
13576         if ( config.expanded ) {
13577                 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
13578         }
13579         if ( config.framed ) {
13580                 this.$element.addClass( 'oo-ui-panelLayout-framed' );
13581         }
13584 /* Setup */
13586 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
13588 /* Static Methods */
13591  * @inheritdoc
13592  */
13593 OO.ui.PanelLayout.static.reusePreInfuseDOM = function ( node, config ) {
13594         config = OO.ui.PanelLayout.super.static.reusePreInfuseDOM( node, config );
13595         if ( config.preserveContent !== false ) {
13596                 config.$content = $( node ).contents();
13597         }
13598         return config;
13601 /* Methods */
13604  * Focus the panel layout
13606  * The default implementation just focuses the first focusable element in the panel
13607  */
13608 OO.ui.PanelLayout.prototype.focus = function () {
13609         OO.ui.findFocusable( this.$element ).focus();
13613  * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
13614  * items), with small margins between them. Convenient when you need to put a number of block-level
13615  * widgets on a single line next to each other.
13617  * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
13619  *     @example
13620  *     // HorizontalLayout with a text input and a label.
13621  *     const layout = new OO.ui.HorizontalLayout( {
13622  *       items: [
13623  *         new OO.ui.LabelWidget( { label: 'Label' } ),
13624  *         new OO.ui.TextInputWidget( { value: 'Text' } )
13625  *       ]
13626  *     } );
13627  *     $( document.body ).append( layout.$element );
13629  * @class
13630  * @extends OO.ui.Layout
13631  * @mixes OO.ui.mixin.GroupElement
13633  * @constructor
13634  * @param {Object} [config] Configuration options
13635  * @param {OO.ui.Widget[]|OO.ui.Layout[]} [config.items] Widgets or other layouts to add to the layout.
13636  */
13637 OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
13638         // Configuration initialization
13639         config = config || {};
13641         // Parent constructor
13642         OO.ui.HorizontalLayout.super.call( this, config );
13644         // Mixin constructors
13645         OO.ui.mixin.GroupElement.call( this, Object.assign( { $group: this.$element }, config ) );
13647         // Initialization
13648         this.$element.addClass( 'oo-ui-horizontalLayout' );
13649         this.addItems( config.items || [] );
13652 /* Setup */
13654 OO.inheritClass( OO.ui.HorizontalLayout, OO.ui.Layout );
13655 OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
13658  * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
13659  * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
13660  * (to adjust the value in increments) to allow the user to enter a number.
13662  *     @example
13663  *     // A NumberInputWidget.
13664  *     const numberInput = new OO.ui.NumberInputWidget( {
13665  *         label: 'NumberInputWidget',
13666  *         input: { value: 5 },
13667  *         min: 1,
13668  *         max: 10
13669  *     } );
13670  *     $( document.body ).append( numberInput.$element );
13672  * @class
13673  * @extends OO.ui.TextInputWidget
13675  * @constructor
13676  * @param {Object} [config] Configuration options
13677  * @param {Object} [config.minusButton] Configuration options to pass to the
13678  *  {@link OO.ui.ButtonWidget decrementing button widget}.
13679  * @param {Object} [config.plusButton] Configuration options to pass to the
13680  *  {@link OO.ui.ButtonWidget incrementing button widget}.
13681  * @param {number} [config.min=-Infinity] Minimum allowed value
13682  * @param {number} [config.max=Infinity] Maximum allowed value
13683  * @param {number|null} [config.step] If specified, the field only accepts values that are multiples of this.
13684  * @param {number} [config.buttonStep=step||1] Delta when using the buttons or Up/Down arrow keys.
13685  *  Defaults to `step` if specified, otherwise `1`.
13686  * @param {number} [config.pageStep=10*buttonStep] Delta when using the Page-up/Page-down keys.
13687  *  Defaults to 10 times `buttonStep`.
13688  * @param {boolean} [config.showButtons=true] Whether to show the plus and minus buttons.
13689  */
13690 OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
13691         const $field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' );
13693         // Configuration initialization
13694         config = Object.assign( {
13695                 min: -Infinity,
13696                 max: Infinity,
13697                 showButtons: true
13698         }, config );
13700         // For backward compatibility
13701         Object.assign( config, config.input );
13702         this.input = this;
13704         // Parent constructor
13705         OO.ui.NumberInputWidget.super.call( this, Object.assign( config, {
13706                 type: 'number'
13707         } ) );
13709         if ( config.showButtons ) {
13710                 this.minusButton = new OO.ui.ButtonWidget( Object.assign(
13711                         {
13712                                 disabled: this.isDisabled(),
13713                                 tabIndex: -1,
13714                                 classes: [ 'oo-ui-numberInputWidget-minusButton' ],
13715                                 icon: 'subtract'
13716                         },
13717                         config.minusButton
13718                 ) );
13719                 this.minusButton.$element.attr( 'aria-hidden', 'true' );
13720                 this.plusButton = new OO.ui.ButtonWidget( Object.assign(
13721                         {
13722                                 disabled: this.isDisabled(),
13723                                 tabIndex: -1,
13724                                 classes: [ 'oo-ui-numberInputWidget-plusButton' ],
13725                                 icon: 'add'
13726                         },
13727                         config.plusButton
13728                 ) );
13729                 this.plusButton.$element.attr( 'aria-hidden', 'true' );
13730         }
13732         // Events
13733         this.$input.on( {
13734                 keydown: this.onKeyDown.bind( this ),
13735                 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
13736         } );
13737         if ( config.showButtons ) {
13738                 this.plusButton.connect( this, {
13739                         click: [ 'onButtonClick', +1 ]
13740                 } );
13741                 this.minusButton.connect( this, {
13742                         click: [ 'onButtonClick', -1 ]
13743                 } );
13744         }
13746         // Build the field
13747         $field.append( this.$input );
13748         if ( config.showButtons ) {
13749                 $field
13750                         .prepend( this.minusButton.$element )
13751                         .append( this.plusButton.$element );
13752         }
13754         // Initialization
13755         if ( config.allowInteger || config.isInteger ) {
13756                 // Backward compatibility
13757                 config.step = 1;
13758         }
13759         this.setRange( config.min, config.max );
13760         this.setStep( config.buttonStep, config.pageStep, config.step );
13761         // Set the validation method after we set step and range
13762         // so that it doesn't immediately call setValidityFlag
13763         this.setValidation( this.validateNumber.bind( this ) );
13765         this.$element
13766                 .addClass( 'oo-ui-numberInputWidget' )
13767                 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config.showButtons )
13768                 .append( $field );
13771 /* Setup */
13773 OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.TextInputWidget );
13775 /* Methods */
13777 // Backward compatibility
13778 OO.ui.NumberInputWidget.prototype.setAllowInteger = function ( flag ) {
13779         this.setStep( flag ? 1 : null );
13781 // Backward compatibility
13782 OO.ui.NumberInputWidget.prototype.setIsInteger = OO.ui.NumberInputWidget.prototype.setAllowInteger;
13784 // Backward compatibility
13785 OO.ui.NumberInputWidget.prototype.getAllowInteger = function () {
13786         return this.step === 1;
13788 // Backward compatibility
13789 OO.ui.NumberInputWidget.prototype.getIsInteger = OO.ui.NumberInputWidget.prototype.getAllowInteger;
13792  * Set the range of allowed values
13794  * @param {number} min Minimum allowed value
13795  * @param {number} max Maximum allowed value
13796  */
13797 OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
13798         if ( min > max ) {
13799                 throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
13800         }
13801         this.min = min;
13802         this.max = max;
13803         this.$input.attr( { min: this.min, max: this.max } );
13804         this.setValidityFlag();
13808  * Get the current range
13810  * @return {number[]} Minimum and maximum values
13811  */
13812 OO.ui.NumberInputWidget.prototype.getRange = function () {
13813         return [ this.min, this.max ];
13817  * Set the stepping deltas
13819  * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
13820  *  Defaults to `step` if specified, otherwise `1`.
13821  * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
13822  *  Defaults to 10 times `buttonStep`.
13823  * @param {number|null} [step] If specified, the field only accepts values that are multiples
13824  *  of this.
13825  */
13826 OO.ui.NumberInputWidget.prototype.setStep = function ( buttonStep, pageStep, step ) {
13827         if ( buttonStep === undefined ) {
13828                 buttonStep = step || 1;
13829         }
13830         if ( pageStep === undefined ) {
13831                 pageStep = 10 * buttonStep;
13832         }
13833         if ( step !== null && step <= 0 ) {
13834                 throw new Error( 'Step value, if given, must be positive' );
13835         }
13836         if ( buttonStep <= 0 ) {
13837                 throw new Error( 'Button step value must be positive' );
13838         }
13839         if ( pageStep <= 0 ) {
13840                 throw new Error( 'Page step value must be positive' );
13841         }
13842         this.step = step;
13843         this.buttonStep = buttonStep;
13844         this.pageStep = pageStep;
13845         this.$input.attr( 'step', this.step || 'any' );
13846         this.setValidityFlag();
13850  * @inheritdoc
13851  */
13852 OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
13853         if ( value === '' ) {
13854                 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
13855                 // so here we make sure an 'empty' value is actually displayed as such.
13856                 this.$input.val( '' );
13857         }
13858         return OO.ui.NumberInputWidget.super.prototype.setValue.call( this, value );
13862  * Get the current stepping values
13864  * @return {number[]} Button step, page step, and validity step
13865  */
13866 OO.ui.NumberInputWidget.prototype.getStep = function () {
13867         return [ this.buttonStep, this.pageStep, this.step ];
13871  * Get the current value of the widget as a number
13873  * @return {number} May be NaN, or an invalid number
13874  */
13875 OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
13876         return +this.getValue();
13880  * Adjust the value of the widget
13882  * @param {number} delta Adjustment amount
13883  */
13884 OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
13885         const v = this.getNumericValue();
13887         delta = +delta;
13888         if ( isNaN( delta ) || !isFinite( delta ) ) {
13889                 throw new Error( 'Delta must be a finite number' );
13890         }
13892         let n;
13893         if ( isNaN( v ) ) {
13894                 n = 0;
13895         } else {
13896                 n = v + delta;
13897                 n = Math.max( Math.min( n, this.max ), this.min );
13898                 if ( this.step ) {
13899                         n = Math.round( n / this.step ) * this.step;
13900                 }
13901         }
13903         if ( n !== v ) {
13904                 this.setValue( n );
13905         }
13908  * Validate input
13910  * @private
13911  * @param {string} value Field value
13912  * @return {boolean}
13913  */
13914 OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
13915         const n = +value;
13916         if ( value === '' ) {
13917                 return !this.isRequired();
13918         }
13920         if ( isNaN( n ) || !isFinite( n ) ) {
13921                 return false;
13922         }
13924         if ( this.step && Math.floor( n / this.step ) !== n / this.step ) {
13925                 return false;
13926         }
13928         if ( n < this.min || n > this.max ) {
13929                 return false;
13930         }
13932         return true;
13936  * Handle mouse click events.
13938  * @private
13939  * @param {number} dir +1 or -1
13940  */
13941 OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
13942         this.adjustValue( dir * this.buttonStep );
13946  * Handle mouse wheel events.
13948  * @private
13949  * @param {jQuery.Event} event
13950  * @return {undefined|boolean} False to prevent default if event is handled
13951  */
13952 OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
13953         let delta = 0;
13955         if ( this.isDisabled() || this.isReadOnly() ) {
13956                 return;
13957         }
13959         if ( this.$input.is( ':focus' ) ) {
13960                 // Standard 'wheel' event
13961                 if ( event.originalEvent.deltaMode !== undefined ) {
13962                         this.sawWheelEvent = true;
13963                 }
13964                 if ( event.originalEvent.deltaY ) {
13965                         delta = -event.originalEvent.deltaY;
13966                 } else if ( event.originalEvent.deltaX ) {
13967                         delta = event.originalEvent.deltaX;
13968                 }
13970                 // Non-standard events
13971                 if ( !this.sawWheelEvent ) {
13972                         if ( event.originalEvent.wheelDeltaX ) {
13973                                 delta = -event.originalEvent.wheelDeltaX;
13974                         } else if ( event.originalEvent.wheelDeltaY ) {
13975                                 delta = event.originalEvent.wheelDeltaY;
13976                         } else if ( event.originalEvent.wheelDelta ) {
13977                                 delta = event.originalEvent.wheelDelta;
13978                         } else if ( event.originalEvent.detail ) {
13979                                 delta = -event.originalEvent.detail;
13980                         }
13981                 }
13983                 if ( delta ) {
13984                         delta = delta < 0 ? -1 : 1;
13985                         this.adjustValue( delta * this.buttonStep );
13986                 }
13988                 return false;
13989         }
13993  * Handle key down events.
13995  * @private
13996  * @param {jQuery.Event} e Key down event
13997  * @return {undefined|boolean} False to prevent default if event is handled
13998  */
13999 OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
14000         if ( this.isDisabled() || this.isReadOnly() ) {
14001                 return;
14002         }
14004         switch ( e.which ) {
14005                 case OO.ui.Keys.UP:
14006                         this.adjustValue( this.buttonStep );
14007                         return false;
14008                 case OO.ui.Keys.DOWN:
14009                         this.adjustValue( -this.buttonStep );
14010                         return false;
14011                 case OO.ui.Keys.PAGEUP:
14012                         this.adjustValue( this.pageStep );
14013                         return false;
14014                 case OO.ui.Keys.PAGEDOWN:
14015                         this.adjustValue( -this.pageStep );
14016                         return false;
14017         }
14021  * Update the disabled state of the controls
14023  * @chainable
14024  * @protected
14025  * @return {OO.ui.NumberInputWidget} The widget, for chaining
14026  */
14027 OO.ui.NumberInputWidget.prototype.updateControlsDisabled = function () {
14028         const disabled = this.isDisabled() || this.isReadOnly();
14029         if ( this.minusButton ) {
14030                 this.minusButton.setDisabled( disabled );
14031         }
14032         if ( this.plusButton ) {
14033                 this.plusButton.setDisabled( disabled );
14034         }
14035         return this;
14039  * @inheritdoc
14040  */
14041 OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
14042         // Parent method
14043         OO.ui.NumberInputWidget.super.prototype.setDisabled.call( this, disabled );
14044         this.updateControlsDisabled();
14045         return this;
14049  * @inheritdoc
14050  */
14051 OO.ui.NumberInputWidget.prototype.setReadOnly = function () {
14052         // Parent method
14053         OO.ui.NumberInputWidget.super.prototype.setReadOnly.apply( this, arguments );
14054         this.updateControlsDisabled();
14055         return this;
14059  * SelectFileInputWidgets allow for selecting files, using <input type="file">. These
14060  * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
14061  * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
14062  * Please see the [OOUI documentation on MediaWiki][1] for more information and examples.
14064  * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename.
14066  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
14068  *     @example
14069  *     // A file select input widget.
14070  *     const selectFile = new OO.ui.SelectFileInputWidget();
14071  *     $( document.body ).append( selectFile.$element );
14073  * @class
14074  * @extends OO.ui.InputWidget
14075  * @mixes OO.ui.mixin.RequiredElement
14076  * @mixes OO.ui.mixin.PendingElement
14078  * @constructor
14079  * @param {Object} [config] Configuration options
14080  * @param {string[]|null} [config.accept=null] MIME types to accept. null accepts all types.
14081  * @param {boolean} [config.multiple=false] Allow multiple files to be selected.
14082  * @param {string} [config.placeholder] Text to display when no file is selected.
14083  * @param {Object} [config.button] Config to pass to select file button.
14084  * @param {Object|string|null} [config.icon=null] Icon to show next to file info
14085  * @param {boolean} [config.droppable=true] Whether to accept files by drag and drop.
14086  * @param {boolean} [config.buttonOnly=false] Show only the select file button, no info field.
14087  *  Requires showDropTarget to be false.
14088  * @param {boolean} [config.showDropTarget=false] Whether to show a drop target. Requires droppable
14089  *  to be true.
14090  * @param {number} [config.thumbnailSizeLimit=20] File size limit in MiB above which to not try and
14091  *  show a preview (for performance).
14092  */
14093 OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
14094         config = config || {};
14096         // Construct buttons before parent method is called (calling setDisabled)
14097         this.selectButton = new OO.ui.ButtonWidget( Object.assign( {
14098                 $element: $( '<label>' ),
14099                 classes: [ 'oo-ui-selectFileInputWidget-selectButton' ],
14100                 label: OO.ui.msg(
14101                         config.multiple ?
14102                                 'ooui-selectfile-button-select-multiple' :
14103                                 'ooui-selectfile-button-select'
14104                 )
14105         }, config.button ) );
14107         // Configuration initialization
14108         config = Object.assign( {
14109                 accept: null,
14110                 placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
14111                 $tabIndexed: this.selectButton.$tabIndexed,
14112                 droppable: true,
14113                 buttonOnly: false,
14114                 showDropTarget: false,
14115                 thumbnailSizeLimit: 20
14116         }, config );
14118         this.canSetFiles = true;
14119         // Support: Safari < 14
14120         try {
14121                 // eslint-disable-next-line no-new
14122                 new DataTransfer();
14123         } catch ( e ) {
14124                 this.canSetFiles = false;
14125                 config.droppable = false;
14126         }
14128         this.info = new OO.ui.SearchInputWidget( {
14129                 classes: [ 'oo-ui-selectFileInputWidget-info' ],
14130                 placeholder: config.placeholder,
14131                 // Pass an empty collection so that .focus() always does nothing
14132                 $tabIndexed: $( [] )
14133         } ).setIcon( config.icon );
14134         // Set tabindex manually on $input as $tabIndexed has been overridden.
14135         // Prevents field from becoming focused while tabbing.
14136         // We will also set the disabled attribute on $input, but that is done in #setDisabled.
14137         this.info.$input.attr( 'tabindex', -1 );
14138         // This indicator serves as the only way to clear the file, so it must be keyboard-accessible
14139         this.info.$indicator.attr( 'tabindex', 0 );
14141         // Parent constructor
14142         OO.ui.SelectFileInputWidget.super.call( this, config );
14144         // Mixin constructors
14145         OO.ui.mixin.RequiredElement.call( this, Object.assign( {}, {
14146                 // TODO: Display the required indicator somewhere
14147                 indicatorElement: null
14148         }, config ) );
14149         OO.ui.mixin.PendingElement.call( this );
14151         // Properties
14152         this.currentFiles = this.filterFiles( this.$input[ 0 ].files || [] );
14153         if ( Array.isArray( config.accept ) ) {
14154                 this.accept = config.accept;
14155         } else {
14156                 this.accept = null;
14157         }
14158         this.multiple = !!config.multiple;
14159         this.showDropTarget = config.droppable && config.showDropTarget;
14160         this.thumbnailSizeLimit = config.thumbnailSizeLimit;
14162         // Initialization
14163         this.fieldLayout = new OO.ui.ActionFieldLayout( this.info, this.selectButton, { align: 'top' } );
14165         this.$input
14166                 .attr( {
14167                         type: 'file',
14168                         // this.selectButton is tabindexed
14169                         tabindex: -1,
14170                         // Infused input may have previously by
14171                         // TabIndexed, so remove aria-disabled attr.
14172                         'aria-disabled': null
14173                 } );
14175         if ( this.accept ) {
14176                 this.$input.attr( 'accept', this.accept.join( ', ' ) );
14177         }
14178         if ( this.multiple ) {
14179                 this.$input.attr( 'multiple', '' );
14180         }
14181         this.selectButton.$button.append( this.$input );
14183         this.$element
14184                 .addClass( 'oo-ui-selectFileInputWidget oo-ui-selectFileWidget' )
14185                 .append( this.fieldLayout.$element );
14187         if ( this.showDropTarget ) {
14188                 this.selectButton.setIcon( 'upload' );
14189                 this.$element
14190                         .addClass( 'oo-ui-selectFileInputWidget-dropTarget oo-ui-selectFileWidget-dropTarget' )
14191                         .on( {
14192                                 click: this.onDropTargetClick.bind( this )
14193                         } )
14194                         .append(
14195                                 this.info.$element,
14196                                 this.selectButton.$element,
14197                                 $( '<span>' )
14198                                         .addClass( 'oo-ui-selectFileInputWidget-dropLabel oo-ui-selectFileWidget-dropLabel' )
14199                                         .text( OO.ui.msg(
14200                                                 this.multiple ?
14201                                                         'ooui-selectfile-dragdrop-placeholder-multiple' :
14202                                                         'ooui-selectfile-dragdrop-placeholder'
14203                                         ) )
14204                         );
14205                 if ( !this.multiple ) {
14206                         this.$thumbnail = $( '<div>' ).addClass( 'oo-ui-selectFileInputWidget-thumbnail oo-ui-selectFileWidget-thumbnail' );
14207                         this.setPendingElement( this.$thumbnail );
14208                         this.$element
14209                                 .addClass( 'oo-ui-selectFileInputWidget-withThumbnail oo-ui-selectFileWidget-withThumbnail' )
14210                                 .prepend( this.$thumbnail );
14211                 }
14212                 this.fieldLayout.$element.remove();
14213         } else if ( config.buttonOnly ) {
14214                 // Copy over any classes that may have been added already.
14215                 // Ensure no events are bound to this.$element before here.
14216                 this.selectButton.$element
14217                         .addClass( this.$element.attr( 'class' ) )
14218                         .addClass( 'oo-ui-selectFileInputWidget-buttonOnly oo-ui-selectFileWidget-buttonOnly' );
14219                 // Set this.$element to just be the button
14220                 this.$element = this.selectButton.$element;
14221         }
14223         // Events
14224         this.info.connect( this, { change: 'onInfoChange' } );
14225         this.selectButton.$button.on( {
14226                 keypress: this.onKeyPress.bind( this )
14227         } );
14228         this.$input.on( {
14229                 change: this.onFileSelected.bind( this ),
14230                 click: function ( e ) {
14231                         // Prevents dropTarget getting clicked which calls
14232                         // a click on this input
14233                         e.stopPropagation();
14234                 }
14235         } );
14237         this.connect( this, { change: 'updateUI' } );
14238         if ( config.droppable ) {
14239                 const dragHandler = this.onDragEnterOrOver.bind( this );
14240                 this.$element.on( {
14241                         dragenter: dragHandler,
14242                         dragover: dragHandler,
14243                         dragleave: this.onDragLeave.bind( this ),
14244                         drop: this.onDrop.bind( this )
14245                 } );
14246         }
14248         this.updateUI();
14251 /* Setup */
14253 OO.inheritClass( OO.ui.SelectFileInputWidget, OO.ui.InputWidget );
14254 OO.mixinClass( OO.ui.SelectFileInputWidget, OO.ui.mixin.RequiredElement );
14255 OO.mixinClass( OO.ui.SelectFileInputWidget, OO.ui.mixin.PendingElement );
14257 /* Events */
14260  * A change event is emitted when the currently selected files change
14262  * @event OO.ui.SelectFileInputWidget#change
14263  * @param {File[]} currentFiles Current file list
14264  */
14266 /* Static Properties */
14268 // Set empty title so that browser default tooltips like "No file chosen" don't appear.
14269 OO.ui.SelectFileInputWidget.static.title = '';
14271 /* Methods */
14274  * Get the current value of the field
14276  * For single file widgets returns a File or null.
14277  * For multiple file widgets returns a list of Files.
14279  * @return {File|File[]|null}
14280  */
14281 OO.ui.SelectFileInputWidget.prototype.getValue = function () {
14282         return this.multiple ? this.currentFiles : this.currentFiles[ 0 ];
14286  * Set the current file list
14288  * Can only be set to a non-null/non-empty value if this.canSetFiles is true,
14289  * or if the widget has been set natively and we are just updating the internal
14290  * state.
14292  * @param {File[]|null} files Files to select
14293  * @chainable
14294  * @return {OO.ui.SelectFileInputWidget} The widget, for chaining
14295  */
14296 OO.ui.SelectFileInputWidget.prototype.setValue = function ( files ) {
14297         if ( files === undefined || typeof files === 'string' ) {
14298                 // Called during init, don't replace value if just infusing.
14299                 return this;
14300         }
14302         if ( files && !this.multiple ) {
14303                 files = files.slice( 0, 1 );
14304         }
14306         function comparableFile( file ) {
14307                 // Use extend to convert to plain objects so they can be compared.
14308                 // File objects contains name, size, timestamp and mime type which
14309                 // should be unique.
14310                 return Object.assign( {}, file );
14311         }
14313         if ( !OO.compare(
14314                 files && files.map( comparableFile ),
14315                 this.currentFiles && this.currentFiles.map( comparableFile )
14316         ) ) {
14317                 this.currentFiles = files || [];
14318                 this.emit( 'change', this.currentFiles );
14319         }
14321         if ( this.canSetFiles ) {
14322                 // Convert File[] array back to FileList for setting DOM value
14323                 const dataTransfer = new DataTransfer();
14324                 Array.prototype.forEach.call( this.currentFiles || [], ( file ) => {
14325                         dataTransfer.items.add( file );
14326                 } );
14327                 this.$input[ 0 ].files = dataTransfer.files;
14328         } else {
14329                 if ( !files || !files.length ) {
14330                         // We're allowed to set the input value to empty string
14331                         // to clear.
14332                         OO.ui.SelectFileInputWidget.super.prototype.setValue.call( this, '' );
14333                 }
14334                 // Otherwise we assume the caller was just calling setValue with the
14335                 // current state of .files in the DOM.
14336         }
14338         return this;
14342  * Get the filename of the currently selected file.
14344  * @return {string} Filename
14345  */
14346 OO.ui.SelectFileInputWidget.prototype.getFilename = function () {
14347         return this.currentFiles.map( ( file ) => file.name ).join( ', ' );
14351  * Handle file selection from the input.
14353  * @protected
14354  * @param {jQuery.Event} e
14355  */
14356 OO.ui.SelectFileInputWidget.prototype.onFileSelected = function ( e ) {
14357         const files = this.filterFiles( e.target.files || [] );
14358         this.setValue( files );
14362  * Disable InputWidget#onEdit listener, onFileSelected is used instead.
14364  * @inheritdoc
14365  */
14366 OO.ui.SelectFileInputWidget.prototype.onEdit = function () {};
14369  * Update the user interface when a file is selected or unselected.
14371  * @protected
14372  */
14373 OO.ui.SelectFileInputWidget.prototype.updateUI = function () {
14374         // Too early
14375         if ( !this.selectButton ) {
14376                 return;
14377         }
14379         this.info.setValue( this.getFilename() );
14381         if ( this.currentFiles.length ) {
14382                 this.$element.removeClass( 'oo-ui-selectFileInputWidget-empty' );
14384                 if ( this.showDropTarget ) {
14385                         if ( !this.multiple ) {
14386                                 this.pushPending();
14387                                 this.loadAndGetImageUrl( this.currentFiles[ 0 ] ).done( ( url ) => {
14388                                         this.$thumbnail.css( 'background-image', 'url( ' + url + ' )' );
14389                                 } ).fail( () => {
14390                                         this.$thumbnail.append(
14391                                                 new OO.ui.IconWidget( {
14392                                                         icon: 'attachment',
14393                                                         classes: [ 'oo-ui-selectFileInputWidget-noThumbnail-icon oo-ui-selectFileWidget-noThumbnail-icon' ]
14394                                                 } ).$element
14395                                         );
14396                                 } ).always( () => {
14397                                         this.popPending();
14398                                 } );
14399                         }
14400                         this.$element.off( 'click' );
14401                 }
14402         } else {
14403                 if ( this.showDropTarget ) {
14404                         this.$element.off( 'click' );
14405                         this.$element.on( {
14406                                 click: this.onDropTargetClick.bind( this )
14407                         } );
14408                         if ( !this.multiple ) {
14409                                 this.$thumbnail
14410                                         .empty()
14411                                         .css( 'background-image', '' );
14412                         }
14413                 }
14414                 this.$element.addClass( 'oo-ui-selectFileInputWidget-empty' );
14415         }
14419  * If the selected file is an image, get its URL and load it.
14421  * @param {File} file File
14422  * @return {jQuery.Promise} Promise resolves with the image URL after it has loaded
14423  */
14424 OO.ui.SelectFileInputWidget.prototype.loadAndGetImageUrl = function ( file ) {
14425         const deferred = $.Deferred(),
14426                 reader = new FileReader();
14428         if (
14429                 ( OO.getProp( file, 'type' ) || '' ).indexOf( 'image/' ) === 0 &&
14430                 file.size < this.thumbnailSizeLimit * 1024 * 1024
14431         ) {
14432                 reader.onload = function ( event ) {
14433                         const img = document.createElement( 'img' );
14434                         img.addEventListener( 'load', () => {
14435                                 if (
14436                                         img.naturalWidth === 0 ||
14437                                         img.naturalHeight === 0 ||
14438                                         img.complete === false
14439                                 ) {
14440                                         deferred.reject();
14441                                 } else {
14442                                         deferred.resolve( event.target.result );
14443                                 }
14444                         } );
14445                         img.src = event.target.result;
14446                 };
14447                 reader.readAsDataURL( file );
14448         } else {
14449                 deferred.reject();
14450         }
14452         return deferred.promise();
14456  * Determine if we should accept this file.
14458  * @private
14459  * @param {FileList|File[]} files Files to filter
14460  * @return {File[]} Filter files
14461  */
14462 OO.ui.SelectFileInputWidget.prototype.filterFiles = function ( files ) {
14463         const accept = this.accept;
14465         function mimeAllowed( file ) {
14466                 const mimeType = file.type;
14468                 if ( !accept || !mimeType ) {
14469                         return true;
14470                 }
14472                 for ( let i = 0; i < accept.length; i++ ) {
14473                         let mimeTest = accept[ i ];
14474                         if ( mimeTest === mimeType ) {
14475                                 return true;
14476                         } else if ( mimeTest.slice( -2 ) === '/*' ) {
14477                                 mimeTest = mimeTest.slice( 0, mimeTest.length - 1 );
14478                                 if ( mimeType.slice( 0, mimeTest.length ) === mimeTest ) {
14479                                         return true;
14480                                 }
14481                         }
14482                 }
14483                 return false;
14484         }
14486         return Array.prototype.filter.call( files, mimeAllowed );
14490  * Handle info input change events
14492  * The info widget can only be changed by the user
14493  * with the clear button.
14495  * @private
14496  * @param {string} value
14497  */
14498 OO.ui.SelectFileInputWidget.prototype.onInfoChange = function ( value ) {
14499         if ( value === '' ) {
14500                 this.setValue( null );
14501         }
14505  * Handle key press events.
14507  * @private
14508  * @param {jQuery.Event} e Key press event
14509  * @return {undefined|boolean} False to prevent default if event is handled
14510  */
14511 OO.ui.SelectFileInputWidget.prototype.onKeyPress = function ( e ) {
14512         if ( !this.isDisabled() && this.$input &&
14513                 ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
14514         ) {
14515                 // Emit a click to open the file selector.
14516                 this.$input.trigger( 'click' );
14517                 // Taking focus from the selectButton means keyUp isn't fired, so fire it manually.
14518                 this.selectButton.onDocumentKeyUp( e );
14519                 return false;
14520         }
14524  * @inheritdoc
14525  */
14526 OO.ui.SelectFileInputWidget.prototype.setDisabled = function ( disabled ) {
14527         // Parent method
14528         OO.ui.SelectFileInputWidget.super.prototype.setDisabled.call( this, disabled );
14530         this.selectButton.setDisabled( disabled );
14531         this.info.setDisabled( disabled );
14533         // Always make the input element disabled, so that it can't be found and focused,
14534         // e.g. by OO.ui.findFocusable.
14535         // The SearchInputWidget can otherwise be enabled normally.
14536         this.info.$input.attr( 'disabled', true );
14538         return this;
14542  * Handle drop target click events.
14544  * @private
14545  * @param {jQuery.Event} e Key press event
14546  * @return {undefined|boolean} False to prevent default if event is handled
14547  */
14548 OO.ui.SelectFileInputWidget.prototype.onDropTargetClick = function () {
14549         if ( !this.isDisabled() && this.$input ) {
14550                 this.$input.trigger( 'click' );
14551                 return false;
14552         }
14556  * Handle drag enter and over events
14558  * @private
14559  * @param {jQuery.Event} e Drag event
14560  * @return {undefined|boolean} False to prevent default if event is handled
14561  */
14562 OO.ui.SelectFileInputWidget.prototype.onDragEnterOrOver = function ( e ) {
14563         let hasDroppableFile = false;
14565         const dt = e.originalEvent.dataTransfer;
14567         e.preventDefault();
14568         e.stopPropagation();
14570         if ( this.isDisabled() ) {
14571                 this.$element.removeClass( [
14572                         'oo-ui-selectFileInputWidget-canDrop',
14573                         'oo-ui-selectFileWidget-canDrop',
14574                         'oo-ui-selectFileInputWidget-cantDrop'
14575                 ] );
14576                 dt.dropEffect = 'none';
14577                 return false;
14578         }
14580         // DataTransferItem and File both have a type property, but in Chrome files
14581         // have no information at this point.
14582         const itemsOrFiles = dt.items || dt.files;
14583         const hasFiles = !!( itemsOrFiles && itemsOrFiles.length );
14584         if ( hasFiles ) {
14585                 if ( this.filterFiles( itemsOrFiles ).length ) {
14586                         hasDroppableFile = true;
14587                 }
14588         // dt.types is Array-like, but not an Array
14589         } else if ( Array.prototype.indexOf.call( OO.getProp( dt, 'types' ) || [], 'Files' ) !== -1 ) {
14590                 // File information is not available at this point for security so just assume
14591                 // it is acceptable for now.
14592                 // https://bugzilla.mozilla.org/show_bug.cgi?id=640534
14593                 hasDroppableFile = true;
14594         }
14596         this.$element.toggleClass( 'oo-ui-selectFileInputWidget-canDrop oo-ui-selectFileWidget-canDrop', hasDroppableFile );
14597         this.$element.toggleClass( 'oo-ui-selectFileInputWidget-cantDrop', !hasDroppableFile && hasFiles );
14598         if ( !hasDroppableFile ) {
14599                 dt.dropEffect = 'none';
14600         }
14602         return false;
14606  * Handle drag leave events
14608  * @private
14609  * @param {jQuery.Event} e Drag event
14610  */
14611 OO.ui.SelectFileInputWidget.prototype.onDragLeave = function () {
14612         this.$element.removeClass( [
14613                 'oo-ui-selectFileInputWidget-canDrop',
14614                 'oo-ui-selectFileWidget-canDrop',
14615                 'oo-ui-selectFileInputWidget-cantDrop'
14616         ] );
14620  * Handle drop events
14622  * @private
14623  * @param {jQuery.Event} e Drop event
14624  * @return {undefined|boolean} False to prevent default if event is handled
14625  */
14626 OO.ui.SelectFileInputWidget.prototype.onDrop = function ( e ) {
14627         const dt = e.originalEvent.dataTransfer;
14629         e.preventDefault();
14630         e.stopPropagation();
14631         this.$element.removeClass( [
14632                 'oo-ui-selectFileInputWidget-canDrop',
14633                 'oo-ui-selectFileWidget-canDrop',
14634                 'oo-ui-selectFileInputWidget-cantDrop'
14635         ] );
14637         if ( this.isDisabled() ) {
14638                 return false;
14639         }
14641         const files = this.filterFiles( dt.files || [] );
14642         this.setValue( files );
14644         return false;
14647 // Deprecated alias
14648 OO.ui.SelectFileWidget = function OoUiSelectFileWidget() {
14649         OO.ui.warnDeprecation( 'SelectFileWidget: Deprecated alias, use SelectFileInputWidget instead.' );
14650         OO.ui.SelectFileWidget.super.apply( this, arguments );
14653 OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.SelectFileInputWidget );
14655 }( OO ) );
14657 //# sourceMappingURL=oojs-ui-core.js.map.json