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