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