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