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