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