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