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