3 * https://www.mediawiki.org/wiki/OOUI
5 * Copyright 2011–2018 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2018-08-01T22:17:59Z
16 * Namespace for all classes, static methods and static properties.
48 * Constants for MouseEvent.which
52 OO
.ui
.MouseButtons
= {
65 * Generate a unique ID for element
69 OO
.ui
.generateElementId = function () {
71 return 'ooui-' + OO
.ui
.elementId
;
75 * Check if an element is focusable.
76 * Inspired by :focusable in jQueryUI v1.11.4 - 2015-04-14
78 * @param {jQuery} $element Element to test
79 * @return {boolean} Element is focusable
81 OO
.ui
.isFocusableElement = function ( $element
) {
83 element
= $element
[ 0 ];
85 // Anything disabled is not focusable
86 if ( element
.disabled
) {
90 // Check if the element is visible
92 // This is quicker than calling $element.is( ':visible' )
93 $.expr
.pseudos
.visible( element
) &&
94 // Check that all parents are visible
95 !$element
.parents().addBack().filter( function () {
96 return $.css( this, 'visibility' ) === 'hidden';
102 // Check if the element is ContentEditable, which is the string 'true'
103 if ( element
.contentEditable
=== 'true' ) {
107 // Anything with a non-negative numeric tabIndex is focusable.
108 // Use .prop to avoid browser bugs
109 if ( $element
.prop( 'tabIndex' ) >= 0 ) {
113 // Some element types are naturally focusable
114 // (indexOf is much faster than regex in Chrome and about the
115 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
116 nodeName
= element
.nodeName
.toLowerCase();
117 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName
) !== -1 ) {
121 // Links and areas are focusable if they have an href
122 if ( ( nodeName
=== 'a' || nodeName
=== 'area' ) && $element
.attr( 'href' ) !== undefined ) {
130 * Find a focusable child
132 * @param {jQuery} $container Container to search in
133 * @param {boolean} [backwards] Search backwards
134 * @return {jQuery} Focusable child, or an empty jQuery object if none found
136 OO
.ui
.findFocusable = function ( $container
, backwards
) {
137 var $focusable
= $( [] ),
138 // $focusableCandidates is a superset of things that
139 // could get matched by isFocusableElement
140 $focusableCandidates
= $container
141 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
144 $focusableCandidates
= Array
.prototype.reverse
.call( $focusableCandidates
);
147 $focusableCandidates
.each( function () {
148 var $this = $( this );
149 if ( OO
.ui
.isFocusableElement( $this ) ) {
158 * Get the user's language and any fallback languages.
160 * These language codes are used to localize user interface elements in the user's language.
162 * In environments that provide a localization system, this function should be overridden to
163 * return the user's language(s). The default implementation returns English (en) only.
165 * @return {string[]} Language codes, in descending order of priority
167 OO
.ui
.getUserLanguages = function () {
172 * Get a value in an object keyed by language code.
174 * @param {Object.<string,Mixed>} obj Object keyed by language code
175 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
176 * @param {string} [fallback] Fallback code, used if no matching language can be found
177 * @return {Mixed} Local value
179 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
182 // Requested language
186 // Known user language
187 langs
= OO
.ui
.getUserLanguages();
188 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
195 if ( obj
[ fallback
] ) {
196 return obj
[ fallback
];
198 // First existing language
199 for ( lang
in obj
) {
207 * Check if a node is contained within another node
209 * Similar to jQuery#contains except a list of containers can be supplied
210 * and a boolean argument allows you to include the container in the match list
212 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
213 * @param {HTMLElement} contained Node to find
214 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
215 * @return {boolean} The node is in the list of target nodes
217 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
219 if ( !Array
.isArray( containers
) ) {
220 containers
= [ containers
];
222 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
223 if ( ( matchContainers
&& contained
=== containers
[ i
] ) || $.contains( containers
[ i
], contained
) ) {
231 * Return a function, that, as long as it continues to be invoked, will not
232 * be triggered. The function will be called after it stops being called for
233 * N milliseconds. If `immediate` is passed, trigger the function on the
234 * leading edge, instead of the trailing.
236 * Ported from: http://underscorejs.org/underscore.js
238 * @param {Function} func Function to debounce
239 * @param {number} [wait=0] Wait period in milliseconds
240 * @param {boolean} [immediate] Trigger on leading edge
241 * @return {Function} Debounced function
243 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
248 later = function () {
251 func
.apply( context
, args
);
254 if ( immediate
&& !timeout
) {
255 func
.apply( context
, args
);
257 if ( !timeout
|| wait
) {
258 clearTimeout( timeout
);
259 timeout
= setTimeout( later
, wait
);
265 * Puts a console warning with provided message.
267 * @param {string} message Message
269 OO
.ui
.warnDeprecation = function ( message
) {
270 if ( OO
.getProp( window
, 'console', 'warn' ) !== undefined ) {
271 // eslint-disable-next-line no-console
272 console
.warn( message
);
277 * Returns a function, that, when invoked, will only be triggered at most once
278 * during a given window of time. If called again during that window, it will
279 * wait until the window ends and then trigger itself again.
281 * As it's not knowable to the caller whether the function will actually run
282 * when the wrapper is called, return values from the function are entirely
285 * @param {Function} func Function to throttle
286 * @param {number} wait Throttle window length, in milliseconds
287 * @return {Function} Throttled function
289 OO
.ui
.throttle = function ( func
, wait
) {
290 var context
, args
, timeout
,
294 previous
= OO
.ui
.now();
295 func
.apply( context
, args
);
298 // Check how long it's been since the last time the function was
299 // called, and whether it's more or less than the requested throttle
300 // period. If it's less, run the function immediately. If it's more,
301 // set a timeout for the remaining time -- but don't replace an
302 // existing timeout, since that'd indefinitely prolong the wait.
303 var remaining
= wait
- ( OO
.ui
.now() - previous
);
306 if ( remaining
<= 0 ) {
307 // Note: unless wait was ridiculously large, this means we'll
308 // automatically run the first time the function was called in a
309 // given period. (If you provide a wait period larger than the
310 // current Unix timestamp, you *deserve* unexpected behavior.)
311 clearTimeout( timeout
);
313 } else if ( !timeout
) {
314 timeout
= setTimeout( run
, remaining
);
320 * A (possibly faster) way to get the current timestamp as an integer
322 * @return {number} Current timestamp, in milliseconds since the Unix epoch
324 OO
.ui
.now
= Date
.now
|| function () {
325 return new Date().getTime();
329 * Reconstitute a JavaScript object corresponding to a widget created by
330 * the PHP implementation.
332 * This is an alias for `OO.ui.Element.static.infuse()`.
334 * @param {string|HTMLElement|jQuery} idOrNode
335 * A DOM id (if a string) or node for the widget to infuse.
336 * @param {Object} [config] Configuration options
337 * @return {OO.ui.Element}
338 * The `OO.ui.Element` corresponding to this (infusable) document node.
340 OO
.ui
.infuse = function ( idOrNode
, config
) {
341 return OO
.ui
.Element
.static.infuse( idOrNode
, config
);
346 * Message store for the default implementation of OO.ui.msg
348 * Environments that provide a localization system should not use this, but should override
349 * OO.ui.msg altogether.
354 // Tool tip for a button that moves items in a list down one place
355 'ooui-outline-control-move-down': 'Move item down',
356 // Tool tip for a button that moves items in a list up one place
357 'ooui-outline-control-move-up': 'Move item up',
358 // Tool tip for a button that removes items from a list
359 'ooui-outline-control-remove': 'Remove item',
360 // Label for the toolbar group that contains a list of all other available tools
361 'ooui-toolbar-more': 'More',
362 // Label for the fake tool that expands the full list of tools in a toolbar group
363 'ooui-toolgroup-expand': 'More',
364 // Label for the fake tool that collapses the full list of tools in a toolbar group
365 'ooui-toolgroup-collapse': 'Fewer',
366 // Default label for the tooltip for the button that removes a tag item
367 'ooui-item-remove': 'Remove',
368 // Default label for the accept button of a confirmation dialog
369 'ooui-dialog-message-accept': 'OK',
370 // Default label for the reject button of a confirmation dialog
371 'ooui-dialog-message-reject': 'Cancel',
372 // Title for process dialog error description
373 'ooui-dialog-process-error': 'Something went wrong',
374 // Label for process dialog dismiss error button, visible when describing errors
375 'ooui-dialog-process-dismiss': 'Dismiss',
376 // Label for process dialog retry action button, visible when describing only recoverable errors
377 'ooui-dialog-process-retry': 'Try again',
378 // Label for process dialog retry action button, visible when describing only warnings
379 'ooui-dialog-process-continue': 'Continue',
380 // Label for the file selection widget's select file button
381 'ooui-selectfile-button-select': 'Select a file',
382 // Label for the file selection widget if file selection is not supported
383 'ooui-selectfile-not-supported': 'File selection is not supported',
384 // Label for the file selection widget when no file is currently selected
385 'ooui-selectfile-placeholder': 'No file is selected',
386 // Label for the file selection widget's drop target
387 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
391 * Get a localized message.
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.
398 * In environments that provide a localization system, this function should be overridden to
399 * return the message translated in the user's language. The default implementation always returns
400 * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
404 * var i, iLen, button,
405 * messagePath = 'oojs-ui/dist/i18n/',
406 * languages = [ $.i18n().locale, 'ur', 'en' ],
409 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
410 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
413 * $.i18n().load( languageMap ).done( function() {
414 * // Replace the built-in `msg` only once we've loaded the internationalization.
415 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
416 * // you put off creating any widgets until this promise is complete, no English
417 * // will be displayed.
418 * OO.ui.msg = $.i18n;
420 * // A button displaying "OK" in the default locale
421 * button = new OO.ui.ButtonWidget( {
422 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
425 * $( 'body' ).append( button.$element );
427 * // A button displaying "OK" in Urdu
428 * $.i18n().locale = 'ur';
429 * button = new OO.ui.ButtonWidget( {
430 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
433 * $( 'body' ).append( button.$element );
436 * @param {string} key Message key
437 * @param {...Mixed} [params] Message parameters
438 * @return {string} Translated message with parameters substituted
440 OO
.ui
.msg = function ( key
) {
441 var message
= messages
[ key
],
442 params
= Array
.prototype.slice
.call( arguments
, 1 );
443 if ( typeof message
=== 'string' ) {
444 // Perform $1 substitution
445 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
446 var i
= parseInt( n
, 10 );
447 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
450 // Return placeholder if message not found
451 message
= '[' + key
+ ']';
458 * Package a message and arguments for deferred resolution.
460 * Use this when you are statically specifying a message and the message may not yet be present.
462 * @param {string} key Message key
463 * @param {...Mixed} [params] Message parameters
464 * @return {Function} Function that returns the resolved message when executed
466 OO
.ui
.deferMsg = function () {
467 var args
= arguments
;
469 return OO
.ui
.msg
.apply( OO
.ui
, args
);
476 * If the message is a function it will be executed, otherwise it will pass through directly.
478 * @param {Function|string} msg Deferred message, or message text
479 * @return {string} Resolved message
481 OO
.ui
.resolveMsg = function ( msg
) {
482 if ( $.isFunction( msg
) ) {
489 * @param {string} url
492 OO
.ui
.isSafeUrl = function ( url
) {
493 // Keep this function in sync with php/Tag.php
494 var i
, protocolWhitelist
;
496 function stringStartsWith( haystack
, needle
) {
497 return haystack
.substr( 0, needle
.length
) === needle
;
500 protocolWhitelist
= [
501 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
502 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
503 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
510 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
511 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
516 // This matches '//' too
517 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
520 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
528 * Check if the user has a 'mobile' device.
530 * For our purposes this means the user is primarily using an
531 * on-screen keyboard, touch input instead of a mouse and may
532 * have a physically small display.
534 * It is left up to implementors to decide how to compute this
535 * so the default implementation always returns false.
537 * @return {boolean} User is on a mobile device
539 OO
.ui
.isMobile = function () {
544 * Get the additional spacing that should be taken into account when displaying elements that are
545 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
546 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
548 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
549 * the extra spacing from that edge of viewport (in pixels)
551 OO
.ui
.getViewportSpacing = function () {
561 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
562 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
564 * @return {jQuery} Default overlay node
566 OO
.ui
.getDefaultOverlay = function () {
567 if ( !OO
.ui
.$defaultOverlay
) {
568 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
569 $( 'body' ).append( OO
.ui
.$defaultOverlay
);
571 return OO
.ui
.$defaultOverlay
;
579 * Namespace for OOUI mixins.
581 * Mixins are named according to the type of object they are intended to
582 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
583 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
584 * is intended to be mixed in to an instance of OO.ui.Widget.
592 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
593 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
594 * connected to them and can't be interacted with.
600 * @param {Object} [config] Configuration options
601 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
602 * to the top level (e.g., the outermost div) of the element. See the [OOUI documentation on MediaWiki][2]
604 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
605 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
606 * @cfg {string} [text] Text to insert
607 * @cfg {Array} [content] An array of content elements to append (after #text).
608 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
609 * Instances of OO.ui.Element will have their $element appended.
610 * @cfg {jQuery} [$content] Content elements to append (after #text).
611 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
612 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
613 * Data can also be specified with the #setData method.
615 OO
.ui
.Element
= function OoUiElement( config
) {
616 if ( OO
.ui
.isDemo
) {
617 this.initialConfig
= config
;
619 // Configuration initialization
620 config
= config
|| {};
624 this.elementId
= null;
626 this.data
= config
.data
;
627 this.$element
= config
.$element
||
628 $( document
.createElement( this.getTagName() ) );
629 this.elementGroup
= null;
632 if ( Array
.isArray( config
.classes
) ) {
633 this.$element
.addClass( config
.classes
.join( ' ' ) );
636 this.setElementId( config
.id
);
639 this.$element
.text( config
.text
);
641 if ( config
.content
) {
642 // The `content` property treats plain strings as text; use an
643 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
644 // appropriate $element appended.
645 this.$element
.append( config
.content
.map( function ( v
) {
646 if ( typeof v
=== 'string' ) {
647 // Escape string so it is properly represented in HTML.
648 return document
.createTextNode( v
);
649 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
652 } else if ( v
instanceof OO
.ui
.Element
) {
658 if ( config
.$content
) {
659 // The `$content` property treats plain strings as HTML.
660 this.$element
.append( config
.$content
);
666 OO
.initClass( OO
.ui
.Element
);
668 /* Static Properties */
671 * The name of the HTML tag used by the element.
673 * The static value may be ignored if the #getTagName method is overridden.
679 OO
.ui
.Element
.static.tagName
= 'div';
684 * Reconstitute a JavaScript object corresponding to a widget created
685 * by the PHP implementation.
687 * @param {string|HTMLElement|jQuery} idOrNode
688 * A DOM id (if a string) or node for the widget to infuse.
689 * @param {Object} [config] Configuration options
690 * @return {OO.ui.Element}
691 * The `OO.ui.Element` corresponding to this (infusable) document node.
692 * For `Tag` objects emitted on the HTML side (used occasionally for content)
693 * the value returned is a newly-created Element wrapping around the existing
696 OO
.ui
.Element
.static.infuse = function ( idOrNode
, config
) {
697 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, config
, false );
698 // Verify that the type matches up.
699 // FIXME: uncomment after T89721 is fixed, see T90929.
701 if ( !( obj instanceof this['class'] ) ) {
702 throw new Error( 'Infusion type mismatch!' );
709 * Implementation helper for `infuse`; skips the type check and has an
710 * extra property so that only the top-level invocation touches the DOM.
713 * @param {string|HTMLElement|jQuery} idOrNode
714 * @param {Object} [config] Configuration options
715 * @param {jQuery.Promise} [domPromise] A promise that will be resolved
716 * when the top-level widget of this infusion is inserted into DOM,
717 * replacing the original node; only used internally.
718 * @return {OO.ui.Element}
720 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, config
, domPromise
) {
721 // look for a cached result of a previous infusion.
722 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
723 if ( typeof idOrNode
=== 'string' ) {
725 $elem
= $( document
.getElementById( id
) );
727 $elem
= $( idOrNode
);
728 id
= $elem
.attr( 'id' );
730 if ( !$elem
.length
) {
731 if ( typeof idOrNode
=== 'string' ) {
732 error
= 'Widget not found: ' + idOrNode
;
733 } else if ( idOrNode
&& idOrNode
.selector
) {
734 error
= 'Widget not found: ' + idOrNode
.selector
;
736 error
= 'Widget not found';
738 throw new Error( error
);
740 if ( $elem
[ 0 ].oouiInfused
) {
741 $elem
= $elem
[ 0 ].oouiInfused
;
743 data
= $elem
.data( 'ooui-infused' );
746 if ( data
=== true ) {
747 throw new Error( 'Circular dependency! ' + id
);
750 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
751 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
752 // restore dynamic state after the new element is re-inserted into DOM under infused parent
753 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
754 infusedChildren
= $elem
.data( 'ooui-infused-children' );
755 if ( infusedChildren
&& infusedChildren
.length
) {
756 infusedChildren
.forEach( function ( data
) {
757 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
758 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
764 data
= $elem
.attr( 'data-ooui' );
766 throw new Error( 'No infusion data found: ' + id
);
769 data
= JSON
.parse( data
);
773 if ( !( data
&& data
._
) ) {
774 throw new Error( 'No valid infusion data found: ' + id
);
776 if ( data
._
=== 'Tag' ) {
777 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
778 return new OO
.ui
.Element( $.extend( {}, config
, { $element
: $elem
} ) );
780 parts
= data
._
.split( '.' );
781 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
782 if ( cls
=== undefined ) {
783 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
786 // Verify that we're creating an OO.ui.Element instance
789 while ( parent
!== undefined ) {
790 if ( parent
=== OO
.ui
.Element
) {
795 parent
= parent
.parent
;
798 if ( parent
!== OO
.ui
.Element
) {
799 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
804 domPromise
= top
.promise();
806 $elem
.data( 'ooui-infused', true ); // prevent loops
807 data
.id
= id
; // implicit
808 infusedChildren
= [];
809 data
= OO
.copy( data
, null, function deserialize( value
) {
811 if ( OO
.isPlainObject( value
) ) {
813 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, config
, domPromise
);
814 infusedChildren
.push( infused
);
815 // Flatten the structure
816 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
817 infused
.$element
.removeData( 'ooui-infused-children' );
820 if ( value
.html
!== undefined ) {
821 return new OO
.ui
.HtmlSnippet( value
.html
);
825 // allow widgets to reuse parts of the DOM
826 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
827 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
828 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
830 // eslint-disable-next-line new-cap
831 obj
= new cls( $.extend( {}, config
, data
) );
832 // If anyone is holding a reference to the old DOM element,
833 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
834 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
835 $elem
[ 0 ].oouiInfused
= obj
.$element
;
836 // now replace old DOM with this new DOM.
838 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
839 // so only mutate the DOM if we need to.
840 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
841 $elem
.replaceWith( obj
.$element
);
845 obj
.$element
.data( 'ooui-infused', obj
);
846 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
847 // set the 'data-ooui' attribute so we can identify infused widgets
848 obj
.$element
.attr( 'data-ooui', '' );
849 // restore dynamic state after the new element is inserted into DOM
850 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
855 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
857 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
858 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
859 * constructor, which will be given the enhanced config.
862 * @param {HTMLElement} node
863 * @param {Object} config
866 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
871 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
872 * (and its children) that represent an Element of the same class and the given configuration,
873 * generated by the PHP implementation.
875 * This method is called just before `node` is detached from the DOM. The return value of this
876 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
877 * is inserted into DOM to replace `node`.
880 * @param {HTMLElement} node
881 * @param {Object} config
884 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
889 * Get a jQuery function within a specific document.
892 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
893 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
895 * @return {Function} Bound jQuery function
897 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
898 function wrapper( selector
) {
899 return $( selector
, wrapper
.context
);
902 wrapper
.context
= this.getDocument( context
);
905 wrapper
.$iframe
= $iframe
;
912 * Get the document of an element.
915 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
916 * @return {HTMLDocument|null} Document object
918 OO
.ui
.Element
.static.getDocument = function ( obj
) {
919 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
920 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
921 // Empty jQuery selections might have a context
928 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
933 * Get the window of an element or document.
936 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
937 * @return {Window} Window object
939 OO
.ui
.Element
.static.getWindow = function ( obj
) {
940 var doc
= this.getDocument( obj
);
941 return doc
.defaultView
;
945 * Get the direction of an element or document.
948 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
949 * @return {string} Text direction, either 'ltr' or 'rtl'
951 OO
.ui
.Element
.static.getDir = function ( obj
) {
954 if ( obj
instanceof jQuery
) {
957 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
958 isWin
= obj
.document
!== undefined;
959 if ( isDoc
|| isWin
) {
965 return $( obj
).css( 'direction' );
969 * Get the offset between two frames.
971 * TODO: Make this function not use recursion.
974 * @param {Window} from Window of the child frame
975 * @param {Window} [to=window] Window of the parent frame
976 * @param {Object} [offset] Offset to start with, used internally
977 * @return {Object} Offset object, containing left and top properties
979 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
980 var i
, len
, frames
, frame
, rect
;
986 offset
= { top
: 0, left
: 0 };
988 if ( from.parent
=== from ) {
992 // Get iframe element
993 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
994 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
995 if ( frames
[ i
].contentWindow
=== from ) {
1001 // Recursively accumulate offset values
1003 rect
= frame
.getBoundingClientRect();
1004 offset
.left
+= rect
.left
;
1005 offset
.top
+= rect
.top
;
1006 if ( from !== to
) {
1007 this.getFrameOffset( from.parent
, offset
);
1014 * Get the offset between two elements.
1016 * The two elements may be in a different frame, but in that case the frame $element is in must
1017 * be contained in the frame $anchor is in.
1020 * @param {jQuery} $element Element whose position to get
1021 * @param {jQuery} $anchor Element to get $element's position relative to
1022 * @return {Object} Translated position coordinates, containing top and left properties
1024 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1025 var iframe
, iframePos
,
1026 pos
= $element
.offset(),
1027 anchorPos
= $anchor
.offset(),
1028 elementDocument
= this.getDocument( $element
),
1029 anchorDocument
= this.getDocument( $anchor
);
1031 // If $element isn't in the same document as $anchor, traverse up
1032 while ( elementDocument
!== anchorDocument
) {
1033 iframe
= elementDocument
.defaultView
.frameElement
;
1035 throw new Error( '$element frame is not contained in $anchor frame' );
1037 iframePos
= $( iframe
).offset();
1038 pos
.left
+= iframePos
.left
;
1039 pos
.top
+= iframePos
.top
;
1040 elementDocument
= iframe
.ownerDocument
;
1042 pos
.left
-= anchorPos
.left
;
1043 pos
.top
-= anchorPos
.top
;
1048 * Get element border sizes.
1051 * @param {HTMLElement} el Element to measure
1052 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1054 OO
.ui
.Element
.static.getBorders = function ( el
) {
1055 var doc
= el
.ownerDocument
,
1056 win
= doc
.defaultView
,
1057 style
= win
.getComputedStyle( el
, null ),
1059 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1060 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1061 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1062 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1073 * Get dimensions of an element or window.
1076 * @param {HTMLElement|Window} el Element to measure
1077 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1079 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1081 doc
= el
.ownerDocument
|| el
.document
,
1082 win
= doc
.defaultView
;
1084 if ( win
=== el
|| el
=== doc
.documentElement
) {
1087 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1089 top
: $win
.scrollTop(),
1090 left
: $win
.scrollLeft()
1092 scrollbar
: { right
: 0, bottom
: 0 },
1096 bottom
: $win
.innerHeight(),
1097 right
: $win
.innerWidth()
1103 borders
: this.getBorders( el
),
1105 top
: $el
.scrollTop(),
1106 left
: $el
.scrollLeft()
1109 right
: $el
.innerWidth() - el
.clientWidth
,
1110 bottom
: $el
.innerHeight() - el
.clientHeight
1112 rect
: el
.getBoundingClientRect()
1118 * Get the number of pixels that an element's content is scrolled to the left.
1120 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1121 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1123 * This function smooths out browser inconsistencies (nicely described in the README at
1124 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1125 * with Firefox's 'scrollLeft', which seems the sanest.
1129 * @param {HTMLElement|Window} el Element to measure
1130 * @return {number} Scroll position from the left.
1131 * If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
1132 * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1133 * If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
1134 * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1136 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1137 var rtlScrollType
= null;
1140 var $definer
= $( '<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>' ),
1141 definer
= $definer
[ 0 ];
1143 $definer
.appendTo( 'body' );
1144 if ( definer
.scrollLeft
> 0 ) {
1146 rtlScrollType
= 'default';
1148 definer
.scrollLeft
= 1;
1149 if ( definer
.scrollLeft
=== 0 ) {
1150 // Firefox, old Opera
1151 rtlScrollType
= 'negative';
1153 // Internet Explorer, Edge
1154 rtlScrollType
= 'reverse';
1160 return function getScrollLeft( el
) {
1161 var isRoot
= el
.window
=== el
||
1162 el
=== el
.ownerDocument
.body
||
1163 el
=== el
.ownerDocument
.documentElement
,
1164 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1165 // All browsers use the correct scroll type ('negative') on the root, so don't
1166 // do any fixups when looking at the root element
1167 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1169 if ( direction
=== 'rtl' ) {
1170 if ( rtlScrollType
=== null ) {
1173 if ( rtlScrollType
=== 'reverse' ) {
1174 scrollLeft
= -scrollLeft
;
1175 } else if ( rtlScrollType
=== 'default' ) {
1176 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1185 * Get the root scrollable element of given element's document.
1187 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1188 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1189 * lets us use 'body' or 'documentElement' based on what is working.
1191 * https://code.google.com/p/chromium/issues/detail?id=303131
1194 * @param {HTMLElement} el Element to find root scrollable parent for
1195 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1196 * depending on browser
1198 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1199 var scrollTop
, body
;
1201 if ( OO
.ui
.scrollableElement
=== undefined ) {
1202 body
= el
.ownerDocument
.body
;
1203 scrollTop
= body
.scrollTop
;
1206 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1207 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1208 if ( Math
.round( body
.scrollTop
) === 1 ) {
1209 body
.scrollTop
= scrollTop
;
1210 OO
.ui
.scrollableElement
= 'body';
1212 OO
.ui
.scrollableElement
= 'documentElement';
1216 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1220 * Get closest scrollable container.
1222 * Traverses up until either a scrollable element or the root is reached, in which case the root
1223 * scrollable element will be returned (see #getRootScrollableElement).
1226 * @param {HTMLElement} el Element to find scrollable container for
1227 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1228 * @return {HTMLElement} Closest scrollable container
1230 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1232 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1233 // 'overflow-y' have different values, so we need to check the separate properties.
1234 props
= [ 'overflow-x', 'overflow-y' ],
1235 $parent
= $( el
).parent();
1237 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1238 props
= [ 'overflow-' + dimension
];
1241 // Special case for the document root (which doesn't really have any scrollable container, since
1242 // it is the ultimate scrollable container, but this is probably saner than null or exception)
1243 if ( $( el
).is( 'html, body' ) ) {
1244 return this.getRootScrollableElement( el
);
1247 while ( $parent
.length
) {
1248 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1249 return $parent
[ 0 ];
1253 val
= $parent
.css( props
[ i
] );
1254 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
1255 // scrolled in that direction, but they can actually be scrolled programatically. The user can
1256 // unintentionally perform a scroll in such case even if the application doesn't scroll
1257 // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
1258 // This could cause funny issues...
1259 if ( val
=== 'auto' || val
=== 'scroll' ) {
1260 return $parent
[ 0 ];
1263 $parent
= $parent
.parent();
1265 // The element is unattached... return something mostly sane
1266 return this.getRootScrollableElement( el
);
1270 * Scroll element into view.
1273 * @param {HTMLElement} el Element to scroll into view
1274 * @param {Object} [config] Configuration options
1275 * @param {string} [config.duration='fast'] jQuery animation duration value
1276 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1277 * to scroll in both directions
1278 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1280 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1281 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1282 deferred
= $.Deferred();
1284 // Configuration initialization
1285 config
= config
|| {};
1288 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1289 $container
= $( container
);
1290 elementDimensions
= this.getDimensions( el
);
1291 containerDimensions
= this.getDimensions( container
);
1292 $window
= $( this.getWindow( el
) );
1294 // Compute the element's position relative to the container
1295 if ( $container
.is( 'html, body' ) ) {
1296 // If the scrollable container is the root, this is easy
1298 top
: elementDimensions
.rect
.top
,
1299 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1300 left
: elementDimensions
.rect
.left
,
1301 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1304 // Otherwise, we have to subtract el's coordinates from container's coordinates
1306 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1307 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1308 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1309 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1313 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1314 if ( position
.top
< 0 ) {
1315 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1316 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1317 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1320 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1321 if ( position
.left
< 0 ) {
1322 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1323 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1324 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1327 if ( !$.isEmptyObject( animations
) ) {
1328 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1329 $container
.queue( function ( next
) {
1336 return deferred
.promise();
1340 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1341 * and reserve space for them, because it probably doesn't.
1343 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1344 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1345 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1346 * and then reattach (or show) them back.
1349 * @param {HTMLElement} el Element to reconsider the scrollbars on
1351 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1352 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1353 // Save scroll position
1354 scrollLeft
= el
.scrollLeft
;
1355 scrollTop
= el
.scrollTop
;
1356 // Detach all children
1357 while ( el
.firstChild
) {
1358 nodes
.push( el
.firstChild
);
1359 el
.removeChild( el
.firstChild
);
1362 void el
.offsetHeight
;
1363 // Reattach all children
1364 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1365 el
.appendChild( nodes
[ i
] );
1367 // Restore scroll position (no-op if scrollbars disappeared)
1368 el
.scrollLeft
= scrollLeft
;
1369 el
.scrollTop
= scrollTop
;
1375 * Toggle visibility of an element.
1377 * @param {boolean} [show] Make element visible, omit to toggle visibility
1381 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1382 show
= show
=== undefined ? !this.visible
: !!show
;
1384 if ( show
!== this.isVisible() ) {
1385 this.visible
= show
;
1386 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1387 this.emit( 'toggle', show
);
1394 * Check if element is visible.
1396 * @return {boolean} element is visible
1398 OO
.ui
.Element
.prototype.isVisible = function () {
1399 return this.visible
;
1405 * @return {Mixed} Element data
1407 OO
.ui
.Element
.prototype.getData = function () {
1414 * @param {Mixed} data Element data
1417 OO
.ui
.Element
.prototype.setData = function ( data
) {
1423 * Set the element has an 'id' attribute.
1425 * @param {string} id
1428 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1429 this.elementId
= id
;
1430 this.$element
.attr( 'id', id
);
1435 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1436 * and return its value.
1440 OO
.ui
.Element
.prototype.getElementId = function () {
1441 if ( this.elementId
=== null ) {
1442 this.setElementId( OO
.ui
.generateElementId() );
1444 return this.elementId
;
1448 * Check if element supports one or more methods.
1450 * @param {string|string[]} methods Method or list of methods to check
1451 * @return {boolean} All methods are supported
1453 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1457 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1458 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1459 if ( $.isFunction( this[ methods
[ i
] ] ) ) {
1464 return methods
.length
=== support
;
1468 * Update the theme-provided classes.
1470 * @localdoc This is called in element mixins and widget classes any time state changes.
1471 * Updating is debounced, minimizing overhead of changing multiple attributes and
1472 * guaranteeing that theme updates do not occur within an element's constructor
1474 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1475 OO
.ui
.theme
.queueUpdateElementClasses( this );
1479 * Get the HTML tag name.
1481 * Override this method to base the result on instance information.
1483 * @return {string} HTML tag name
1485 OO
.ui
.Element
.prototype.getTagName = function () {
1486 return this.constructor.static.tagName
;
1490 * Check if the element is attached to the DOM
1492 * @return {boolean} The element is attached to the DOM
1494 OO
.ui
.Element
.prototype.isElementAttached = function () {
1495 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1499 * Get the DOM document.
1501 * @return {HTMLDocument} Document object
1503 OO
.ui
.Element
.prototype.getElementDocument = function () {
1504 // Don't cache this in other ways either because subclasses could can change this.$element
1505 return OO
.ui
.Element
.static.getDocument( this.$element
);
1509 * Get the DOM window.
1511 * @return {Window} Window object
1513 OO
.ui
.Element
.prototype.getElementWindow = function () {
1514 return OO
.ui
.Element
.static.getWindow( this.$element
);
1518 * Get closest scrollable container.
1520 * @return {HTMLElement} Closest scrollable container
1522 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1523 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1527 * Get group element is in.
1529 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1531 OO
.ui
.Element
.prototype.getElementGroup = function () {
1532 return this.elementGroup
;
1536 * Set group element is in.
1538 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1541 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1542 this.elementGroup
= group
;
1547 * Scroll element into view.
1549 * @param {Object} [config] Configuration options
1550 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1552 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1554 !this.isElementAttached() ||
1555 !this.isVisible() ||
1556 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1558 return $.Deferred().resolve();
1560 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1564 * Restore the pre-infusion dynamic state for this widget.
1566 * This method is called after #$element has been inserted into DOM. The parameter is the return
1567 * value of #gatherPreInfuseState.
1570 * @param {Object} state
1572 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1576 * Wraps an HTML snippet for use with configuration values which default
1577 * to strings. This bypasses the default html-escaping done to string
1583 * @param {string} [content] HTML content
1585 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1587 this.content
= content
;
1592 OO
.initClass( OO
.ui
.HtmlSnippet
);
1599 * @return {string} Unchanged HTML snippet.
1601 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1602 return this.content
;
1606 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1607 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1608 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1609 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1610 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1614 * @extends OO.ui.Element
1615 * @mixins OO.EventEmitter
1618 * @param {Object} [config] Configuration options
1620 OO
.ui
.Layout
= function OoUiLayout( config
) {
1621 // Configuration initialization
1622 config
= config
|| {};
1624 // Parent constructor
1625 OO
.ui
.Layout
.parent
.call( this, config
);
1627 // Mixin constructors
1628 OO
.EventEmitter
.call( this );
1631 this.$element
.addClass( 'oo-ui-layout' );
1636 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1637 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1640 * Widgets are compositions of one or more OOUI elements that users can both view
1641 * and interact with. All widgets can be configured and modified via a standard API,
1642 * and their state can change dynamically according to a model.
1646 * @extends OO.ui.Element
1647 * @mixins OO.EventEmitter
1650 * @param {Object} [config] Configuration options
1651 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1652 * appearance reflects this state.
1654 OO
.ui
.Widget
= function OoUiWidget( config
) {
1655 // Initialize config
1656 config
= $.extend( { disabled
: false }, config
);
1658 // Parent constructor
1659 OO
.ui
.Widget
.parent
.call( this, config
);
1661 // Mixin constructors
1662 OO
.EventEmitter
.call( this );
1665 this.disabled
= null;
1666 this.wasDisabled
= null;
1669 this.$element
.addClass( 'oo-ui-widget' );
1670 this.setDisabled( !!config
.disabled
);
1675 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1676 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1683 * A 'disable' event is emitted when the disabled state of the widget changes
1684 * (i.e. on disable **and** enable).
1686 * @param {boolean} disabled Widget is disabled
1692 * A 'toggle' event is emitted when the visibility of the widget changes.
1694 * @param {boolean} visible Widget is visible
1700 * Check if the widget is disabled.
1702 * @return {boolean} Widget is disabled
1704 OO
.ui
.Widget
.prototype.isDisabled = function () {
1705 return this.disabled
;
1709 * Set the 'disabled' state of the widget.
1711 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1713 * @param {boolean} disabled Disable widget
1716 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1719 this.disabled
= !!disabled
;
1720 isDisabled
= this.isDisabled();
1721 if ( isDisabled
!== this.wasDisabled
) {
1722 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1723 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1724 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1725 this.emit( 'disable', isDisabled
);
1726 this.updateThemeClasses();
1728 this.wasDisabled
= isDisabled
;
1734 * Update the disabled state, in case of changes in parent widget.
1738 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1739 this.setDisabled( this.disabled
);
1744 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1747 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1750 * @return {string|null} The ID of the labelable element
1752 OO
.ui
.Widget
.prototype.getInputId = function () {
1757 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1758 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1759 * override this method to provide intuitive, accessible behavior.
1761 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1762 * Individual widgets may override it too.
1764 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1767 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1778 OO
.ui
.Theme
= function OoUiTheme() {
1779 this.elementClassesQueue
= [];
1780 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1785 OO
.initClass( OO
.ui
.Theme
);
1790 * Get a list of classes to be applied to a widget.
1792 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1793 * otherwise state transitions will not work properly.
1795 * @param {OO.ui.Element} element Element for which to get classes
1796 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1798 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1799 return { on
: [], off
: [] };
1803 * Update CSS classes provided by the theme.
1805 * For elements with theme logic hooks, this should be called any time there's a state change.
1807 * @param {OO.ui.Element} element Element for which to update classes
1809 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1810 var $elements
= $( [] ),
1811 classes
= this.getElementClasses( element
);
1813 if ( element
.$icon
) {
1814 $elements
= $elements
.add( element
.$icon
);
1816 if ( element
.$indicator
) {
1817 $elements
= $elements
.add( element
.$indicator
);
1821 .removeClass( classes
.off
.join( ' ' ) )
1822 .addClass( classes
.on
.join( ' ' ) );
1828 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1830 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1831 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1834 this.elementClassesQueue
= [];
1838 * Queue #updateElementClasses to be called for this element.
1840 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1841 * to make them synchronous.
1843 * @param {OO.ui.Element} element Element for which to update classes
1845 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1846 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1847 // the most common case (this method is often called repeatedly for the same element).
1848 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1851 this.elementClassesQueue
.push( element
);
1852 this.debouncedUpdateQueuedElementClasses();
1856 * Get the transition duration in milliseconds for dialogs opening/closing
1858 * The dialog should be fully rendered this many milliseconds after the
1859 * ready process has executed.
1861 * @return {number} Transition duration in milliseconds
1863 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1868 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1869 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1870 * order in which users will navigate through the focusable elements via the "tab" key.
1873 * // TabIndexedElement is mixed into the ButtonWidget class
1874 * // to provide a tabIndex property.
1875 * var button1 = new OO.ui.ButtonWidget( {
1879 * var button2 = new OO.ui.ButtonWidget( {
1883 * var button3 = new OO.ui.ButtonWidget( {
1887 * var button4 = new OO.ui.ButtonWidget( {
1891 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1897 * @param {Object} [config] Configuration options
1898 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1899 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1900 * functionality will be applied to it instead.
1901 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1902 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1903 * to remove the element from the tab-navigation flow.
1905 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1906 // Configuration initialization
1907 config
= $.extend( { tabIndex
: 0 }, config
);
1910 this.$tabIndexed
= null;
1911 this.tabIndex
= null;
1914 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1917 this.setTabIndex( config
.tabIndex
);
1918 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1923 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1928 * Set the element that should use the tabindex functionality.
1930 * This method is used to retarget a tabindex mixin so that its functionality applies
1931 * to the specified element. If an element is currently using the functionality, the mixin’s
1932 * effect on that element is removed before the new element is set up.
1934 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1937 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1938 var tabIndex
= this.tabIndex
;
1939 // Remove attributes from old $tabIndexed
1940 this.setTabIndex( null );
1941 // Force update of new $tabIndexed
1942 this.$tabIndexed
= $tabIndexed
;
1943 this.tabIndex
= tabIndex
;
1944 return this.updateTabIndex();
1948 * Set the value of the tabindex.
1950 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1953 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1954 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
1956 if ( this.tabIndex
!== tabIndex
) {
1957 this.tabIndex
= tabIndex
;
1958 this.updateTabIndex();
1965 * Update the `tabindex` attribute, in case of changes to tab index or
1971 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
1972 if ( this.$tabIndexed
) {
1973 if ( this.tabIndex
!== null ) {
1974 // Do not index over disabled elements
1975 this.$tabIndexed
.attr( {
1976 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
1977 // Support: ChromeVox and NVDA
1978 // These do not seem to inherit aria-disabled from parent elements
1979 'aria-disabled': this.isDisabled().toString()
1982 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
1989 * Handle disable events.
1992 * @param {boolean} disabled Element is disabled
1994 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
1995 this.updateTabIndex();
1999 * Get the value of the tabindex.
2001 * @return {number|null} Tabindex value
2003 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2004 return this.tabIndex
;
2008 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2010 * If the element already has an ID then that is returned, otherwise unique ID is
2011 * generated, set on the element, and returned.
2013 * @return {string|null} The ID of the focusable element
2015 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2018 if ( !this.$tabIndexed
) {
2021 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2025 id
= this.$tabIndexed
.attr( 'id' );
2026 if ( id
=== undefined ) {
2027 id
= OO
.ui
.generateElementId();
2028 this.$tabIndexed
.attr( 'id', id
);
2035 * Whether the node is 'labelable' according to the HTML spec
2036 * (i.e., whether it can be interacted with through a `<label for="…">`).
2037 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2040 * @param {jQuery} $node
2043 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2045 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2046 tagName
= $node
.prop( 'tagName' ).toLowerCase();
2048 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2051 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2058 * Focus this element.
2062 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2063 if ( !this.isDisabled() ) {
2064 this.$tabIndexed
.focus();
2070 * Blur this element.
2074 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2075 this.$tabIndexed
.blur();
2080 * @inheritdoc OO.ui.Widget
2082 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2087 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2088 * interface element that can be configured with access keys for accessibility.
2089 * See the [OOUI documentation on MediaWiki] [1] for examples.
2091 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2097 * @param {Object} [config] Configuration options
2098 * @cfg {jQuery} [$button] The button element created by the class.
2099 * If this configuration is omitted, the button element will use a generated `<a>`.
2100 * @cfg {boolean} [framed=true] Render the button with a frame
2102 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2103 // Configuration initialization
2104 config
= config
|| {};
2107 this.$button
= null;
2109 this.active
= config
.active
!== undefined && config
.active
;
2110 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
2111 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2112 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2113 this.onKeyUpHandler
= this.onKeyUp
.bind( this );
2114 this.onClickHandler
= this.onClick
.bind( this );
2115 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2118 this.$element
.addClass( 'oo-ui-buttonElement' );
2119 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2120 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2125 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2127 /* Static Properties */
2130 * Cancel mouse down events.
2132 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
2133 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
2134 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
2139 * @property {boolean}
2141 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2146 * A 'click' event is emitted when the button element is clicked.
2154 * Set the button element.
2156 * This method is used to retarget a button mixin so that its functionality applies to
2157 * the specified button element instead of the one created by the class. If a button element
2158 * is already set, the method will remove the mixin’s effect on that element.
2160 * @param {jQuery} $button Element to use as button
2162 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2163 if ( this.$button
) {
2165 .removeClass( 'oo-ui-buttonElement-button' )
2166 .removeAttr( 'role accesskey' )
2168 mousedown
: this.onMouseDownHandler
,
2169 keydown
: this.onKeyDownHandler
,
2170 click
: this.onClickHandler
,
2171 keypress
: this.onKeyPressHandler
2175 this.$button
= $button
2176 .addClass( 'oo-ui-buttonElement-button' )
2178 mousedown
: this.onMouseDownHandler
,
2179 keydown
: this.onKeyDownHandler
,
2180 click
: this.onClickHandler
,
2181 keypress
: this.onKeyPressHandler
2184 // Add `role="button"` on `<a>` elements, where it's needed
2185 // `toUppercase()` is added for XHTML documents
2186 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2187 this.$button
.attr( 'role', 'button' );
2192 * Handles mouse down events.
2195 * @param {jQuery.Event} e Mouse down event
2197 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2198 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2201 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2202 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2203 // reliably remove the pressed class
2204 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
2205 // Prevent change of focus unless specifically configured otherwise
2206 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2212 * Handles mouse up events.
2215 * @param {MouseEvent} e Mouse up event
2217 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function ( e
) {
2218 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2221 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2222 // Stop listening for mouseup, since we only needed this once
2223 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
2227 * Handles mouse click events.
2230 * @param {jQuery.Event} e Mouse click event
2233 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2234 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2235 if ( this.emit( 'click' ) ) {
2242 * Handles key down events.
2245 * @param {jQuery.Event} e Key down event
2247 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2248 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2251 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2252 // Run the keyup handler no matter where the key is when the button is let go, so we can
2253 // reliably remove the pressed class
2254 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler
, true );
2258 * Handles key up events.
2261 * @param {KeyboardEvent} e Key up event
2263 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function ( e
) {
2264 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2267 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2268 // Stop listening for keyup, since we only needed this once
2269 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler
, true );
2273 * Handles key press events.
2276 * @param {jQuery.Event} e Key press event
2279 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2280 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2281 if ( this.emit( 'click' ) ) {
2288 * Check if button has a frame.
2290 * @return {boolean} Button is framed
2292 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2297 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2299 * @param {boolean} [framed] Make button framed, omit to toggle
2302 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2303 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2304 if ( framed
!== this.framed
) {
2305 this.framed
= framed
;
2307 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2308 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2309 this.updateThemeClasses();
2316 * Set the button's active state.
2318 * The active state can be set on:
2320 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2321 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2322 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2325 * @param {boolean} value Make button active
2328 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2329 this.active
= !!value
;
2330 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2331 this.updateThemeClasses();
2336 * Check if the button is active
2339 * @return {boolean} The button is active
2341 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2346 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2347 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2348 * items from the group is done through the interface the class provides.
2349 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2351 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2354 * @mixins OO.EmitterList
2358 * @param {Object} [config] Configuration options
2359 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2360 * is omitted, the group element will use a generated `<div>`.
2362 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2363 // Configuration initialization
2364 config
= config
|| {};
2366 // Mixin constructors
2367 OO
.EmitterList
.call( this, config
);
2373 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2378 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2385 * A change event is emitted when the set of selected items changes.
2387 * @param {OO.ui.Element[]} items Items currently in the group
2393 * Set the group element.
2395 * If an element is already set, items will be moved to the new element.
2397 * @param {jQuery} $group Element to use as group
2399 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2402 this.$group
= $group
;
2403 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2404 this.$group
.append( this.items
[ i
].$element
);
2409 * Find an item by its data.
2411 * Only the first item with matching data will be returned. To return all matching items,
2412 * use the #findItemsFromData method.
2414 * @param {Object} data Item data to search for
2415 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2417 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2419 hash
= OO
.getHash( data
);
2421 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2422 item
= this.items
[ i
];
2423 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2432 * Find items by their data.
2434 * All items with matching data will be returned. To return only the first match, use the #findItemFromData method instead.
2436 * @param {Object} data Item data to search for
2437 * @return {OO.ui.Element[]} Items with equivalent data
2439 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2441 hash
= OO
.getHash( data
),
2444 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2445 item
= this.items
[ i
];
2446 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2455 * Add items to the group.
2457 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2458 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2460 * @param {OO.ui.Element[]} items An array of items to add to the group
2461 * @param {number} [index] Index of the insertion point
2464 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2466 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2468 this.emit( 'change', this.getItems() );
2475 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2476 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2477 this.insertItemElements( items
, newIndex
);
2480 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2488 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2489 item
.setElementGroup( this );
2490 this.insertItemElements( item
, index
);
2493 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2499 * Insert elements into the group
2502 * @param {OO.ui.Element} itemWidget Item to insert
2503 * @param {number} index Insertion index
2505 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2506 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2507 this.$group
.append( itemWidget
.$element
);
2508 } else if ( index
=== 0 ) {
2509 this.$group
.prepend( itemWidget
.$element
);
2511 this.items
[ index
].$element
.before( itemWidget
.$element
);
2516 * Remove the specified items from a group.
2518 * Removed items are detached (not removed) from the DOM so that they may be reused.
2519 * To remove all items from a group, you may wish to use the #clearItems method instead.
2521 * @param {OO.ui.Element[]} items An array of items to remove
2524 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2525 var i
, len
, item
, index
;
2527 // Remove specific items elements
2528 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2530 index
= this.items
.indexOf( item
);
2531 if ( index
!== -1 ) {
2532 item
.setElementGroup( null );
2533 item
.$element
.detach();
2538 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2540 this.emit( 'change', this.getItems() );
2545 * Clear all items from the group.
2547 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2548 * To remove only a subset of items from a group, use the #removeItems method.
2552 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2555 // Remove all item elements
2556 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2557 this.items
[ i
].setElementGroup( null );
2558 this.items
[ i
].$element
.detach();
2562 OO
.EmitterList
.prototype.clearItems
.call( this );
2564 this.emit( 'change', this.getItems() );
2569 * IconElement is often mixed into other classes to generate an icon.
2570 * Icons are graphics, about the size of normal text. They are used to aid the user
2571 * in locating a control or to convey information in a space-efficient way. See the
2572 * [OOUI documentation on MediaWiki] [1] for a list of icons
2573 * included in the library.
2575 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2581 * @param {Object} [config] Configuration options
2582 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2583 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2584 * the icon element be set to an existing icon instead of the one generated by this class, set a
2585 * value using a jQuery selection. For example:
2587 * // Use a <div> tag instead of a <span>
2589 * // Use an existing icon element instead of the one generated by the class
2590 * $icon: this.$element
2591 * // Use an icon element from a child widget
2592 * $icon: this.childwidget.$element
2593 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2594 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2595 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2596 * by the user's language.
2598 * Example of an i18n map:
2600 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2601 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2602 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2603 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2604 * text. The icon title is displayed when users move the mouse over the icon.
2606 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2607 // Configuration initialization
2608 config
= config
|| {};
2613 this.iconTitle
= null;
2616 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2617 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2618 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2623 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2625 /* Static Properties */
2628 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2629 * for i18n purposes and contains a `default` icon name and additional names keyed by
2630 * language code. The `default` name is used when no icon is keyed by the user's language.
2632 * Example of an i18n map:
2634 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2636 * Note: the static property will be overridden if the #icon configuration is used.
2640 * @property {Object|string}
2642 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2645 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2646 * function that returns title text, or `null` for no title.
2648 * The static property will be overridden if the #iconTitle configuration is used.
2652 * @property {string|Function|null}
2654 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2659 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2660 * applies to the specified icon element instead of the one created by the class. If an icon
2661 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2662 * and mixin methods will no longer affect the element.
2664 * @param {jQuery} $icon Element to use as icon
2666 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2669 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2670 .removeAttr( 'title' );
2674 .addClass( 'oo-ui-iconElement-icon' )
2675 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
2676 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2677 if ( this.iconTitle
!== null ) {
2678 this.$icon
.attr( 'title', this.iconTitle
);
2681 this.updateThemeClasses();
2685 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2686 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2689 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2690 * by language code, or `null` to remove the icon.
2693 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2694 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2695 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2697 if ( this.icon
!== icon
) {
2699 if ( this.icon
!== null ) {
2700 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2702 if ( icon
!== null ) {
2703 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2709 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2711 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
2713 this.updateThemeClasses();
2719 * Set the icon title. Use `null` to remove the title.
2721 * @param {string|Function|null} iconTitle A text string used as the icon title,
2722 * a function that returns title text, or `null` for no title.
2725 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
2727 ( typeof iconTitle
=== 'function' || ( typeof iconTitle
=== 'string' && iconTitle
.length
) ) ?
2728 OO
.ui
.resolveMsg( iconTitle
) : null;
2730 if ( this.iconTitle
!== iconTitle
) {
2731 this.iconTitle
= iconTitle
;
2733 if ( this.iconTitle
!== null ) {
2734 this.$icon
.attr( 'title', iconTitle
);
2736 this.$icon
.removeAttr( 'title' );
2745 * Get the symbolic name of the icon.
2747 * @return {string} Icon name
2749 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
2754 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2756 * @return {string} Icon title text
2758 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
2759 return this.iconTitle
;
2763 * IndicatorElement is often mixed into other classes to generate an indicator.
2764 * Indicators are small graphics that are generally used in two ways:
2766 * - To draw attention to the status of an item. For example, an indicator might be
2767 * used to show that an item in a list has errors that need to be resolved.
2768 * - To clarify the function of a control that acts in an exceptional way (a button
2769 * that opens a menu instead of performing an action directly, for example).
2771 * For a list of indicators included in the library, please see the
2772 * [OOUI documentation on MediaWiki] [1].
2774 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2780 * @param {Object} [config] Configuration options
2781 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2782 * configuration is omitted, the indicator element will use a generated `<span>`.
2783 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
2784 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
2786 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2787 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2788 * or a function that returns title text. The indicator title is displayed when users move
2789 * the mouse over the indicator.
2791 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
2792 // Configuration initialization
2793 config
= config
|| {};
2796 this.$indicator
= null;
2797 this.indicator
= null;
2798 this.indicatorTitle
= null;
2801 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
2802 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
2803 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
2808 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
2810 /* Static Properties */
2813 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
2814 * The static property will be overridden if the #indicator configuration is used.
2818 * @property {string|null}
2820 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
2823 * A text string used as the indicator title, a function that returns title text, or `null`
2824 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2828 * @property {string|Function|null}
2830 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
2835 * Set the indicator element.
2837 * If an element is already set, it will be cleaned up before setting up the new element.
2839 * @param {jQuery} $indicator Element to use as indicator
2841 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
2842 if ( this.$indicator
) {
2844 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
2845 .removeAttr( 'title' );
2848 this.$indicator
= $indicator
2849 .addClass( 'oo-ui-indicatorElement-indicator' )
2850 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
2851 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
2852 if ( this.indicatorTitle
!== null ) {
2853 this.$indicator
.attr( 'title', this.indicatorTitle
);
2856 this.updateThemeClasses();
2860 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null` to remove the indicator.
2862 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2865 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
2866 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
2868 if ( this.indicator
!== indicator
) {
2869 if ( this.$indicator
) {
2870 if ( this.indicator
!== null ) {
2871 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
2873 if ( indicator
!== null ) {
2874 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
2877 this.indicator
= indicator
;
2880 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
2881 if ( this.$indicator
) {
2882 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
2884 this.updateThemeClasses();
2890 * Set the indicator title.
2892 * The title is displayed when a user moves the mouse over the indicator.
2894 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
2895 * `null` for no indicator title
2898 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
2900 ( typeof indicatorTitle
=== 'function' || ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ) ?
2901 OO
.ui
.resolveMsg( indicatorTitle
) : null;
2903 if ( this.indicatorTitle
!== indicatorTitle
) {
2904 this.indicatorTitle
= indicatorTitle
;
2905 if ( this.$indicator
) {
2906 if ( this.indicatorTitle
!== null ) {
2907 this.$indicator
.attr( 'title', indicatorTitle
);
2909 this.$indicator
.removeAttr( 'title' );
2918 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
2920 * @return {string} Symbolic name of indicator
2922 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
2923 return this.indicator
;
2927 * Get the indicator title.
2929 * The title is displayed when a user moves the mouse over the indicator.
2931 * @return {string} Indicator title text
2933 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
2934 return this.indicatorTitle
;
2938 * LabelElement is often mixed into other classes to generate a label, which
2939 * helps identify the function of an interface element.
2940 * See the [OOUI documentation on MediaWiki] [1] for more information.
2942 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2948 * @param {Object} [config] Configuration options
2949 * @cfg {jQuery} [$label] The label element created by the class. If this
2950 * configuration is omitted, the label element will use a generated `<span>`.
2951 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2952 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2953 * in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2954 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2956 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2957 // Configuration initialization
2958 config
= config
|| {};
2965 this.setLabel( config
.label
|| this.constructor.static.label
);
2966 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2971 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2976 * @event labelChange
2977 * @param {string} value
2980 /* Static Properties */
2983 * The label text. The label can be specified as a plaintext string, a function that will
2984 * produce a string in the future, or `null` for no label. The static value will
2985 * be overridden if a label is specified with the #label config option.
2989 * @property {string|Function|null}
2991 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2993 /* Static methods */
2996 * Highlight the first occurrence of the query in the given text
2998 * @param {string} text Text
2999 * @param {string} query Query to find
3000 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
3001 * @return {jQuery} Text with the first match of the query
3002 * sub-string wrapped in highlighted span
3004 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
3007 $result
= $( '<span>' );
3011 qLen
= query
.length
;
3012 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
3013 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
3018 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
3021 if ( !query
.length
|| offset
=== -1 ) {
3022 $result
.text( text
);
3025 document
.createTextNode( text
.slice( 0, offset
) ),
3027 .addClass( 'oo-ui-labelElement-label-highlight' )
3028 .text( text
.slice( offset
, offset
+ query
.length
) ),
3029 document
.createTextNode( text
.slice( offset
+ query
.length
) )
3032 return $result
.contents();
3038 * Set the label element.
3040 * If an element is already set, it will be cleaned up before setting up the new element.
3042 * @param {jQuery} $label Element to use as label
3044 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
3045 if ( this.$label
) {
3046 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
3049 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
3050 this.setLabelContent( this.label
);
3056 * An empty string will result in the label being hidden. A string containing only whitespace will
3057 * be converted to a single ` `.
3059 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
3060 * text; or null for no label
3063 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
3064 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
3065 label
= ( ( typeof label
=== 'string' || label
instanceof jQuery
) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
3067 if ( this.label
!== label
) {
3068 if ( this.$label
) {
3069 this.setLabelContent( label
);
3072 this.emit( 'labelChange' );
3075 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
);
3081 * Set the label as plain text with a highlighted query
3083 * @param {string} text Text label to set
3084 * @param {string} query Substring of text to highlight
3085 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
3088 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
3089 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
3095 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
3096 * text; or null for no label
3098 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
3103 * Set the content of the label.
3105 * Do not call this method until after the label element has been set by #setLabelElement.
3108 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
3109 * text; or null for no label
3111 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
3112 if ( typeof label
=== 'string' ) {
3113 if ( label
.match( /^\s*$/ ) ) {
3114 // Convert whitespace only string to a single non-breaking space
3115 this.$label
.html( ' ' );
3117 this.$label
.text( label
);
3119 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
3120 this.$label
.html( label
.toString() );
3121 } else if ( label
instanceof jQuery
) {
3122 this.$label
.empty().append( label
);
3124 this.$label
.empty();
3129 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3130 * additional functionality to an element created by another class. The class provides
3131 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3132 * which are used to customize the look and feel of a widget to better describe its
3133 * importance and functionality.
3135 * The library currently contains the following styling flags for general use:
3137 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
3138 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
3140 * The flags affect the appearance of the buttons:
3143 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3144 * var button1 = new OO.ui.ButtonWidget( {
3145 * label: 'Progressive',
3146 * flags: 'progressive'
3148 * var button2 = new OO.ui.ButtonWidget( {
3149 * label: 'Destructive',
3150 * flags: 'destructive'
3152 * $( 'body' ).append( button1.$element, button2.$element );
3154 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
3155 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3157 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3163 * @param {Object} [config] Configuration options
3164 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary') to apply.
3165 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3166 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3167 * @cfg {jQuery} [$flagged] The flagged element. By default,
3168 * the flagged functionality is applied to the element created by the class ($element).
3169 * If a different element is specified, the flagged functionality will be applied to it instead.
3171 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3172 // Configuration initialization
3173 config
= config
|| {};
3177 this.$flagged
= null;
3180 this.setFlags( config
.flags
);
3181 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3188 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3189 * parameter contains the name of each modified flag and indicates whether it was
3192 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3193 * that the flag was added, `false` that the flag was removed.
3199 * Set the flagged element.
3201 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3202 * If an element is already set, the method will remove the mixin’s effect on that element.
3204 * @param {jQuery} $flagged Element that should be flagged
3206 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3207 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3208 return 'oo-ui-flaggedElement-' + flag
;
3211 if ( this.$flagged
) {
3212 this.$flagged
.removeClass( classNames
);
3215 this.$flagged
= $flagged
.addClass( classNames
);
3219 * Check if the specified flag is set.
3221 * @param {string} flag Name of flag
3222 * @return {boolean} The flag is set
3224 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3225 // This may be called before the constructor, thus before this.flags is set
3226 return this.flags
&& ( flag
in this.flags
);
3230 * Get the names of all flags set.
3232 * @return {string[]} Flag names
3234 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3235 // This may be called before the constructor, thus before this.flags is set
3236 return Object
.keys( this.flags
|| {} );
3245 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3246 var flag
, className
,
3249 classPrefix
= 'oo-ui-flaggedElement-';
3251 for ( flag
in this.flags
) {
3252 className
= classPrefix
+ flag
;
3253 changes
[ flag
] = false;
3254 delete this.flags
[ flag
];
3255 remove
.push( className
);
3258 if ( this.$flagged
) {
3259 this.$flagged
.removeClass( remove
.join( ' ' ) );
3262 this.updateThemeClasses();
3263 this.emit( 'flag', changes
);
3269 * Add one or more flags.
3271 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3272 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3273 * be added (`true`) or removed (`false`).
3277 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3278 var i
, len
, flag
, className
,
3282 classPrefix
= 'oo-ui-flaggedElement-';
3284 if ( typeof flags
=== 'string' ) {
3285 className
= classPrefix
+ flags
;
3287 if ( !this.flags
[ flags
] ) {
3288 this.flags
[ flags
] = true;
3289 add
.push( className
);
3291 } else if ( Array
.isArray( flags
) ) {
3292 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3294 className
= classPrefix
+ flag
;
3296 if ( !this.flags
[ flag
] ) {
3297 changes
[ flag
] = true;
3298 this.flags
[ flag
] = true;
3299 add
.push( className
);
3302 } else if ( OO
.isPlainObject( flags
) ) {
3303 for ( flag
in flags
) {
3304 className
= classPrefix
+ flag
;
3305 if ( flags
[ flag
] ) {
3307 if ( !this.flags
[ flag
] ) {
3308 changes
[ flag
] = true;
3309 this.flags
[ flag
] = true;
3310 add
.push( className
);
3314 if ( this.flags
[ flag
] ) {
3315 changes
[ flag
] = false;
3316 delete this.flags
[ flag
];
3317 remove
.push( className
);
3323 if ( this.$flagged
) {
3325 .addClass( add
.join( ' ' ) )
3326 .removeClass( remove
.join( ' ' ) );
3329 this.updateThemeClasses();
3330 this.emit( 'flag', changes
);
3336 * TitledElement is mixed into other classes to provide a `title` attribute.
3337 * Titles are rendered by the browser and are made visible when the user moves
3338 * the mouse over the element. Titles are not visible on touch devices.
3341 * // TitledElement provides a 'title' attribute to the
3342 * // ButtonWidget class
3343 * var button = new OO.ui.ButtonWidget( {
3344 * label: 'Button with Title',
3345 * title: 'I am a button'
3347 * $( 'body' ).append( button.$element );
3353 * @param {Object} [config] Configuration options
3354 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3355 * If this config is omitted, the title functionality is applied to $element, the
3356 * element created by the class.
3357 * @cfg {string|Function} [title] The title text or a function that returns text. If
3358 * this config is omitted, the value of the {@link #static-title static title} property is used.
3360 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3361 // Configuration initialization
3362 config
= config
|| {};
3365 this.$titled
= null;
3369 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3370 this.setTitledElement( config
.$titled
|| this.$element
);
3375 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3377 /* Static Properties */
3380 * The title text, a function that returns text, or `null` for no title. The value of the static property
3381 * is overridden if the #title config option is used.
3385 * @property {string|Function|null}
3387 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3392 * Set the titled element.
3394 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3395 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3397 * @param {jQuery} $titled Element that should use the 'titled' functionality
3399 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3400 if ( this.$titled
) {
3401 this.$titled
.removeAttr( 'title' );
3404 this.$titled
= $titled
;
3413 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3416 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3417 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3418 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3420 if ( this.title
!== title
) {
3429 * Update the title attribute, in case of changes to title or accessKey.
3434 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3435 var title
= this.getTitle();
3436 if ( this.$titled
) {
3437 if ( title
!== null ) {
3438 // Only if this is an AccessKeyedElement
3439 if ( this.formatTitleWithAccessKey
) {
3440 title
= this.formatTitleWithAccessKey( title
);
3442 this.$titled
.attr( 'title', title
);
3444 this.$titled
.removeAttr( 'title' );
3453 * @return {string} Title string
3455 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3460 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3461 * Accesskeys allow an user to go to a specific element by using
3462 * a shortcut combination of a browser specific keys + the key
3466 * // AccessKeyedElement provides an 'accesskey' attribute to the
3467 * // ButtonWidget class
3468 * var button = new OO.ui.ButtonWidget( {
3469 * label: 'Button with Accesskey',
3472 * $( 'body' ).append( button.$element );
3478 * @param {Object} [config] Configuration options
3479 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3480 * If this config is omitted, the accesskey functionality is applied to $element, the
3481 * element created by the class.
3482 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3483 * this config is omitted, no accesskey will be added.
3485 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3486 // Configuration initialization
3487 config
= config
|| {};
3490 this.$accessKeyed
= null;
3491 this.accessKey
= null;
3494 this.setAccessKey( config
.accessKey
|| null );
3495 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3497 // If this is also a TitledElement and it initialized before we did, we may have
3498 // to update the title with the access key
3499 if ( this.updateTitle
) {
3506 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3508 /* Static Properties */
3511 * The access key, a function that returns a key, or `null` for no accesskey.
3515 * @property {string|Function|null}
3517 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3522 * Set the accesskeyed element.
3524 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3525 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3527 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3529 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3530 if ( this.$accessKeyed
) {
3531 this.$accessKeyed
.removeAttr( 'accesskey' );
3534 this.$accessKeyed
= $accessKeyed
;
3535 if ( this.accessKey
) {
3536 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3543 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3546 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3547 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3549 if ( this.accessKey
!== accessKey
) {
3550 if ( this.$accessKeyed
) {
3551 if ( accessKey
!== null ) {
3552 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3554 this.$accessKeyed
.removeAttr( 'accesskey' );
3557 this.accessKey
= accessKey
;
3559 // Only if this is a TitledElement
3560 if ( this.updateTitle
) {
3571 * @return {string} accessKey string
3573 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3574 return this.accessKey
;
3578 * Add information about the access key to the element's tooltip label.
3579 * (This is only public for hacky usage in FieldLayout.)
3581 * @param {string} title Tooltip label for `title` attribute
3584 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3587 if ( !this.$accessKeyed
) {
3588 // Not initialized yet; the constructor will call updateTitle() which will rerun this function
3591 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the single key
3592 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3593 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3595 accessKey
= this.getAccessKey();
3598 title
+= ' [' + accessKey
+ ']';
3604 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3605 * feels, and functionality can be customized via the class’s configuration options
3606 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3609 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3612 * // A button widget
3613 * var button = new OO.ui.ButtonWidget( {
3614 * label: 'Button with Icon',
3618 * $( 'body' ).append( button.$element );
3620 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3623 * @extends OO.ui.Widget
3624 * @mixins OO.ui.mixin.ButtonElement
3625 * @mixins OO.ui.mixin.IconElement
3626 * @mixins OO.ui.mixin.IndicatorElement
3627 * @mixins OO.ui.mixin.LabelElement
3628 * @mixins OO.ui.mixin.TitledElement
3629 * @mixins OO.ui.mixin.FlaggedElement
3630 * @mixins OO.ui.mixin.TabIndexedElement
3631 * @mixins OO.ui.mixin.AccessKeyedElement
3634 * @param {Object} [config] Configuration options
3635 * @cfg {boolean} [active=false] Whether button should be shown as active
3636 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3637 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3638 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3640 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3641 // Configuration initialization
3642 config
= config
|| {};
3644 // Parent constructor
3645 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3647 // Mixin constructors
3648 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3649 OO
.ui
.mixin
.IconElement
.call( this, config
);
3650 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3651 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3652 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3653 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3654 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3655 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3660 this.noFollow
= false;
3663 this.connect( this, { disable
: 'onDisable' } );
3666 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3668 .addClass( 'oo-ui-buttonWidget' )
3669 .append( this.$button
);
3670 this.setActive( config
.active
);
3671 this.setHref( config
.href
);
3672 this.setTarget( config
.target
);
3673 this.setNoFollow( config
.noFollow
);
3678 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3679 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3680 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3681 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3682 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3683 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3684 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3685 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3686 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3688 /* Static Properties */
3694 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3700 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3705 * Get hyperlink location.
3707 * @return {string} Hyperlink location
3709 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3714 * Get hyperlink target.
3716 * @return {string} Hyperlink target
3718 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3723 * Get search engine traversal hint.
3725 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3727 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3728 return this.noFollow
;
3732 * Set hyperlink location.
3734 * @param {string|null} href Hyperlink location, null to remove
3736 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3737 href
= typeof href
=== 'string' ? href
: null;
3738 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3742 if ( href
!== this.href
) {
3751 * Update the `href` attribute, in case of changes to href or
3757 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3758 if ( this.href
!== null && !this.isDisabled() ) {
3759 this.$button
.attr( 'href', this.href
);
3761 this.$button
.removeAttr( 'href' );
3768 * Handle disable events.
3771 * @param {boolean} disabled Element is disabled
3773 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3778 * Set hyperlink target.
3780 * @param {string|null} target Hyperlink target, null to remove
3782 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3783 target
= typeof target
=== 'string' ? target
: null;
3785 if ( target
!== this.target
) {
3786 this.target
= target
;
3787 if ( target
!== null ) {
3788 this.$button
.attr( 'target', target
);
3790 this.$button
.removeAttr( 'target' );
3798 * Set search engine traversal hint.
3800 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3802 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3803 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3805 if ( noFollow
!== this.noFollow
) {
3806 this.noFollow
= noFollow
;
3808 this.$button
.attr( 'rel', 'nofollow' );
3810 this.$button
.removeAttr( 'rel' );
3817 // Override method visibility hints from ButtonElement
3828 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3829 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3830 * removed, and cleared from the group.
3833 * // Example: A ButtonGroupWidget with two buttons
3834 * var button1 = new OO.ui.PopupButtonWidget( {
3835 * label: 'Select a category',
3838 * $content: $( '<p>List of categories...</p>' ),
3843 * var button2 = new OO.ui.ButtonWidget( {
3846 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3847 * items: [button1, button2]
3849 * $( 'body' ).append( buttonGroup.$element );
3852 * @extends OO.ui.Widget
3853 * @mixins OO.ui.mixin.GroupElement
3856 * @param {Object} [config] Configuration options
3857 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3859 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3860 // Configuration initialization
3861 config
= config
|| {};
3863 // Parent constructor
3864 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3866 // Mixin constructors
3867 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
3870 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3871 if ( Array
.isArray( config
.items
) ) {
3872 this.addItems( config
.items
);
3878 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3879 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3881 /* Static Properties */
3887 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
3896 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
3897 if ( !this.isDisabled() ) {
3898 if ( this.items
[ 0 ] ) {
3899 this.items
[ 0 ].focus();
3908 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
3913 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3914 * which creates a label that identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
3915 * for a list of icons included in the library.
3918 * // An icon widget with a label
3919 * var myIcon = new OO.ui.IconWidget( {
3923 * // Create a label.
3924 * var iconLabel = new OO.ui.LabelWidget( {
3927 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3929 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
3932 * @extends OO.ui.Widget
3933 * @mixins OO.ui.mixin.IconElement
3934 * @mixins OO.ui.mixin.TitledElement
3935 * @mixins OO.ui.mixin.FlaggedElement
3938 * @param {Object} [config] Configuration options
3940 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
3941 // Configuration initialization
3942 config
= config
|| {};
3944 // Parent constructor
3945 OO
.ui
.IconWidget
.parent
.call( this, config
);
3947 // Mixin constructors
3948 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
3949 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
3950 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
3953 this.$element
.addClass( 'oo-ui-iconWidget' );
3958 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
3959 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
3960 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
3961 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
3963 /* Static Properties */
3969 OO
.ui
.IconWidget
.static.tagName
= 'span';
3972 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3973 * attention to the status of an item or to clarify the function within a control. For a list of
3974 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
3977 * // Example of an indicator widget
3978 * var indicator1 = new OO.ui.IndicatorWidget( {
3979 * indicator: 'required'
3982 * // Create a fieldset layout to add a label
3983 * var fieldset = new OO.ui.FieldsetLayout();
3984 * fieldset.addItems( [
3985 * new OO.ui.FieldLayout( indicator1, { label: 'A required indicator:' } )
3987 * $( 'body' ).append( fieldset.$element );
3989 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3992 * @extends OO.ui.Widget
3993 * @mixins OO.ui.mixin.IndicatorElement
3994 * @mixins OO.ui.mixin.TitledElement
3997 * @param {Object} [config] Configuration options
3999 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4000 // Configuration initialization
4001 config
= config
|| {};
4003 // Parent constructor
4004 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4006 // Mixin constructors
4007 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
4008 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
4011 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4016 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4017 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4018 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4020 /* Static Properties */
4026 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4029 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4030 * be configured with a `label` option that is set to a string, a label node, or a function:
4032 * - String: a plaintext string
4033 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4034 * label that includes a link or special styling, such as a gray color or additional graphical elements.
4035 * - Function: a function that will produce a string in the future. Functions are used
4036 * in cases where the value of the label is not currently defined.
4038 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
4039 * will come into focus when the label is clicked.
4042 * // Examples of LabelWidgets
4043 * var label1 = new OO.ui.LabelWidget( {
4044 * label: 'plaintext label'
4046 * var label2 = new OO.ui.LabelWidget( {
4047 * label: $( '<a href="default.html">jQuery label</a>' )
4049 * // Create a fieldset layout with fields for each example
4050 * var fieldset = new OO.ui.FieldsetLayout();
4051 * fieldset.addItems( [
4052 * new OO.ui.FieldLayout( label1 ),
4053 * new OO.ui.FieldLayout( label2 )
4055 * $( 'body' ).append( fieldset.$element );
4058 * @extends OO.ui.Widget
4059 * @mixins OO.ui.mixin.LabelElement
4060 * @mixins OO.ui.mixin.TitledElement
4063 * @param {Object} [config] Configuration options
4064 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4065 * Clicking the label will focus the specified input field.
4067 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4068 // Configuration initialization
4069 config
= config
|| {};
4071 // Parent constructor
4072 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4074 // Mixin constructors
4075 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
4076 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4079 this.input
= config
.input
;
4083 if ( this.input
.getInputId() ) {
4084 this.$element
.attr( 'for', this.input
.getInputId() );
4086 this.$label
.on( 'click', function () {
4087 this.input
.simulateLabelClick();
4091 this.$element
.addClass( 'oo-ui-labelWidget' );
4096 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4097 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4098 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4100 /* Static Properties */
4106 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4109 * PendingElement is a mixin that is used to create elements that notify users that something is happening
4110 * and that they should wait before proceeding. The pending state is visually represented with a pending
4111 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
4112 * field of a {@link OO.ui.TextInputWidget text input widget}.
4114 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
4115 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
4116 * in process dialogs.
4119 * function MessageDialog( config ) {
4120 * MessageDialog.parent.call( this, config );
4122 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4124 * MessageDialog.static.name = 'myMessageDialog';
4125 * MessageDialog.static.actions = [
4126 * { action: 'save', label: 'Done', flags: 'primary' },
4127 * { label: 'Cancel', flags: 'safe' }
4130 * MessageDialog.prototype.initialize = function () {
4131 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4132 * this.content = new OO.ui.PanelLayout( { padded: true } );
4133 * 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>' );
4134 * this.$body.append( this.content.$element );
4136 * MessageDialog.prototype.getBodyHeight = function () {
4139 * MessageDialog.prototype.getActionProcess = function ( action ) {
4140 * var dialog = this;
4141 * if ( action === 'save' ) {
4142 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4143 * return new OO.ui.Process()
4145 * .next( function () {
4146 * dialog.getActions().get({actions: 'save'})[0].popPending();
4149 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4152 * var windowManager = new OO.ui.WindowManager();
4153 * $( 'body' ).append( windowManager.$element );
4155 * var dialog = new MessageDialog();
4156 * windowManager.addWindows( [ dialog ] );
4157 * windowManager.openWindow( dialog );
4163 * @param {Object} [config] Configuration options
4164 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4166 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4167 // Configuration initialization
4168 config
= config
|| {};
4172 this.$pending
= null;
4175 this.setPendingElement( config
.$pending
|| this.$element
);
4180 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4185 * Set the pending element (and clean up any existing one).
4187 * @param {jQuery} $pending The element to set to pending.
4189 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4190 if ( this.$pending
) {
4191 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4194 this.$pending
= $pending
;
4195 if ( this.pending
> 0 ) {
4196 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4201 * Check if an element is pending.
4203 * @return {boolean} Element is pending
4205 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4206 return !!this.pending
;
4210 * Increase the pending counter. The pending state will remain active until the counter is zero
4211 * (i.e., the number of calls to #pushPending and #popPending is the same).
4215 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4216 if ( this.pending
=== 0 ) {
4217 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4218 this.updateThemeClasses();
4226 * Decrease the pending counter. The pending state will remain active until the counter is zero
4227 * (i.e., the number of calls to #pushPending and #popPending is the same).
4231 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4232 if ( this.pending
=== 1 ) {
4233 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4234 this.updateThemeClasses();
4236 this.pending
= Math
.max( 0, this.pending
- 1 );
4242 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4243 * in the document (for example, in an OO.ui.Window's $overlay).
4245 * The elements's position is automatically calculated and maintained when window is resized or the
4246 * page is scrolled. If you reposition the container manually, you have to call #position to make
4247 * sure the element is still placed correctly.
4249 * As positioning is only possible when both the element and the container are attached to the DOM
4250 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4251 * the #toggle method to display a floating popup, for example.
4257 * @param {Object} [config] Configuration options
4258 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4259 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4260 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4261 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4262 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4263 * 'top': Align the top edge with $floatableContainer's top edge
4264 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4265 * 'center': Vertically align the center with $floatableContainer's center
4266 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4267 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4268 * 'after': Directly after $floatableContainer, algining f's start edge with fC's end edge
4269 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4270 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4271 * 'center': Horizontally align the center with $floatableContainer's center
4272 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4275 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4276 // Configuration initialization
4277 config
= config
|| {};
4280 this.$floatable
= null;
4281 this.$floatableContainer
= null;
4282 this.$floatableWindow
= null;
4283 this.$floatableClosestScrollable
= null;
4284 this.floatableOutOfView
= false;
4285 this.onFloatableScrollHandler
= this.position
.bind( this );
4286 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4289 this.setFloatableContainer( config
.$floatableContainer
);
4290 this.setFloatableElement( config
.$floatable
|| this.$element
);
4291 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4292 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4293 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ? true : !!config
.hideWhenOutOfView
;
4299 * Set floatable element.
4301 * If an element is already set, it will be cleaned up before setting up the new element.
4303 * @param {jQuery} $floatable Element to make floatable
4305 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4306 if ( this.$floatable
) {
4307 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4308 this.$floatable
.css( { left
: '', top
: '' } );
4311 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4316 * Set floatable container.
4318 * The element will be positioned relative to the specified container.
4320 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4322 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4323 this.$floatableContainer
= $floatableContainer
;
4324 if ( this.$floatable
) {
4330 * Change how the element is positioned vertically.
4332 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4334 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4335 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4336 throw new Error( 'Invalid value for vertical position: ' + position
);
4338 if ( this.verticalPosition
!== position
) {
4339 this.verticalPosition
= position
;
4340 if ( this.$floatable
) {
4347 * Change how the element is positioned horizontally.
4349 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4351 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4352 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4353 throw new Error( 'Invalid value for horizontal position: ' + position
);
4355 if ( this.horizontalPosition
!== position
) {
4356 this.horizontalPosition
= position
;
4357 if ( this.$floatable
) {
4364 * Toggle positioning.
4366 * Do not turn positioning on until after the element is attached to the DOM and visible.
4368 * @param {boolean} [positioning] Enable positioning, omit to toggle
4371 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4372 var closestScrollableOfContainer
;
4374 if ( !this.$floatable
|| !this.$floatableContainer
) {
4378 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4380 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4381 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4382 this.warnedUnattached
= true;
4385 if ( this.positioning
!== positioning
) {
4386 this.positioning
= positioning
;
4388 this.needsCustomPosition
=
4389 this.verticalPostion
!== 'below' ||
4390 this.horizontalPosition
!== 'start' ||
4391 !OO
.ui
.contains( this.$floatableContainer
[ 0 ], this.$floatable
[ 0 ] );
4393 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4394 // If the scrollable is the root, we have to listen to scroll events
4395 // on the window because of browser inconsistencies.
4396 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4397 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4400 if ( positioning
) {
4401 this.$floatableWindow
= $( this.getElementWindow() );
4402 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4404 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4405 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4407 // Initial position after visible
4410 if ( this.$floatableWindow
) {
4411 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4412 this.$floatableWindow
= null;
4415 if ( this.$floatableClosestScrollable
) {
4416 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4417 this.$floatableClosestScrollable
= null;
4420 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4428 * Check whether the bottom edge of the given element is within the viewport of the given container.
4431 * @param {jQuery} $element
4432 * @param {jQuery} $container
4435 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4436 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
, rightEdgeInBounds
,
4437 startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4438 direction
= $element
.css( 'direction' );
4440 elemRect
= $element
[ 0 ].getBoundingClientRect();
4441 if ( $container
[ 0 ] === window
) {
4442 viewportSpacing
= OO
.ui
.getViewportSpacing();
4446 right
: document
.documentElement
.clientWidth
,
4447 bottom
: document
.documentElement
.clientHeight
4449 contRect
.top
+= viewportSpacing
.top
;
4450 contRect
.left
+= viewportSpacing
.left
;
4451 contRect
.right
-= viewportSpacing
.right
;
4452 contRect
.bottom
-= viewportSpacing
.bottom
;
4454 contRect
= $container
[ 0 ].getBoundingClientRect();
4457 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4458 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4459 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4460 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4461 if ( direction
=== 'rtl' ) {
4462 startEdgeInBounds
= rightEdgeInBounds
;
4463 endEdgeInBounds
= leftEdgeInBounds
;
4465 startEdgeInBounds
= leftEdgeInBounds
;
4466 endEdgeInBounds
= rightEdgeInBounds
;
4469 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4472 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4475 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4478 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4482 // The other positioning values are all about being inside the container,
4483 // so in those cases all we care about is that any part of the container is visible.
4484 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4485 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4489 * Check if the floatable is hidden to the user because it was offscreen.
4491 * @return {boolean} Floatable is out of view
4493 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4494 return this.floatableOutOfView
;
4498 * Position the floatable below its container.
4500 * This should only be done when both of them are attached to the DOM and visible.
4504 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4505 if ( !this.positioning
) {
4510 // To continue, some things need to be true:
4511 // The element must actually be in the DOM
4512 this.isElementAttached() && (
4513 // The closest scrollable is the current window
4514 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4515 // OR is an element in the element's DOM
4516 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4519 // Abort early if important parts of the widget are no longer attached to the DOM
4523 this.floatableOutOfView
= this.hideWhenOutOfView
&& !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4524 if ( this.floatableOutOfView
) {
4525 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4528 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4531 if ( !this.needsCustomPosition
) {
4535 this.$floatable
.css( this.computePosition() );
4537 // We updated the position, so re-evaluate the clipping state.
4538 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4539 // will not notice the need to update itself.)
4540 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4541 // it not listen to the right events in the right places?
4550 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4551 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4552 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4554 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4556 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4557 var isBody
, scrollableX
, scrollableY
, containerPos
,
4558 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4559 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4560 direction
= this.$floatableContainer
.css( 'direction' ),
4561 $offsetParent
= this.$floatable
.offsetParent();
4563 if ( $offsetParent
.is( 'html' ) ) {
4564 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4565 // <html> element, but they do work on the <body>
4566 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4568 isBody
= $offsetParent
.is( 'body' );
4569 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' || $offsetParent
.css( 'overflow-x' ) === 'auto';
4570 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' || $offsetParent
.css( 'overflow-y' ) === 'auto';
4572 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4573 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4574 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4575 // or if it isn't scrollable
4576 scrollTop
= scrollableY
&& !isBody
? $offsetParent
.scrollTop() : 0;
4577 scrollLeft
= scrollableX
&& !isBody
? OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4579 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4580 // if the <body> has a margin
4581 containerPos
= isBody
?
4582 this.$floatableContainer
.offset() :
4583 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4584 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4585 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4586 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4587 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4589 if ( this.verticalPosition
=== 'below' ) {
4590 newPos
.top
= containerPos
.bottom
;
4591 } else if ( this.verticalPosition
=== 'above' ) {
4592 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4593 } else if ( this.verticalPosition
=== 'top' ) {
4594 newPos
.top
= containerPos
.top
;
4595 } else if ( this.verticalPosition
=== 'bottom' ) {
4596 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4597 } else if ( this.verticalPosition
=== 'center' ) {
4598 newPos
.top
= containerPos
.top
+
4599 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4602 if ( this.horizontalPosition
=== 'before' ) {
4603 newPos
.end
= containerPos
.start
;
4604 } else if ( this.horizontalPosition
=== 'after' ) {
4605 newPos
.start
= containerPos
.end
;
4606 } else if ( this.horizontalPosition
=== 'start' ) {
4607 newPos
.start
= containerPos
.start
;
4608 } else if ( this.horizontalPosition
=== 'end' ) {
4609 newPos
.end
= containerPos
.end
;
4610 } else if ( this.horizontalPosition
=== 'center' ) {
4611 newPos
.left
= containerPos
.left
+
4612 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4615 if ( newPos
.start
!== undefined ) {
4616 if ( direction
=== 'rtl' ) {
4617 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.start
;
4619 newPos
.left
= newPos
.start
;
4621 delete newPos
.start
;
4623 if ( newPos
.end
!== undefined ) {
4624 if ( direction
=== 'rtl' ) {
4625 newPos
.left
= newPos
.end
;
4627 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.end
;
4632 // Account for scroll position
4633 if ( newPos
.top
!== '' ) {
4634 newPos
.top
+= scrollTop
;
4636 if ( newPos
.bottom
!== '' ) {
4637 newPos
.bottom
-= scrollTop
;
4639 if ( newPos
.left
!== '' ) {
4640 newPos
.left
+= scrollLeft
;
4642 if ( newPos
.right
!== '' ) {
4643 newPos
.right
-= scrollLeft
;
4646 // Account for scrollbar gutter
4647 if ( newPos
.bottom
!== '' ) {
4648 newPos
.bottom
-= horizScrollbarHeight
;
4650 if ( direction
=== 'rtl' ) {
4651 if ( newPos
.left
!== '' ) {
4652 newPos
.left
-= vertScrollbarWidth
;
4655 if ( newPos
.right
!== '' ) {
4656 newPos
.right
-= vertScrollbarWidth
;
4664 * Element that can be automatically clipped to visible boundaries.
4666 * Whenever the element's natural height changes, you have to call
4667 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4668 * clipping correctly.
4670 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4671 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4672 * then #$clippable will be given a fixed reduced height and/or width and will be made
4673 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4674 * but you can build a static footer by setting #$clippableContainer to an element that contains
4675 * #$clippable and the footer.
4681 * @param {Object} [config] Configuration options
4682 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4683 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4684 * omit to use #$clippable
4686 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4687 // Configuration initialization
4688 config
= config
|| {};
4691 this.$clippable
= null;
4692 this.$clippableContainer
= null;
4693 this.clipping
= false;
4694 this.clippedHorizontally
= false;
4695 this.clippedVertically
= false;
4696 this.$clippableScrollableContainer
= null;
4697 this.$clippableScroller
= null;
4698 this.$clippableWindow
= null;
4699 this.idealWidth
= null;
4700 this.idealHeight
= null;
4701 this.onClippableScrollHandler
= this.clip
.bind( this );
4702 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4705 if ( config
.$clippableContainer
) {
4706 this.setClippableContainer( config
.$clippableContainer
);
4708 this.setClippableElement( config
.$clippable
|| this.$element
);
4714 * Set clippable element.
4716 * If an element is already set, it will be cleaned up before setting up the new element.
4718 * @param {jQuery} $clippable Element to make clippable
4720 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4721 if ( this.$clippable
) {
4722 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4723 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4724 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4727 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4732 * Set clippable container.
4734 * This is the container that will be measured when deciding whether to clip. When clipping,
4735 * #$clippable will be resized in order to keep the clippable container fully visible.
4737 * If the clippable container is unset, #$clippable will be used.
4739 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4741 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4742 this.$clippableContainer
= $clippableContainer
;
4743 if ( this.$clippable
) {
4751 * Do not turn clipping on until after the element is attached to the DOM and visible.
4753 * @param {boolean} [clipping] Enable clipping, omit to toggle
4756 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4757 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4759 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4760 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4761 this.warnedUnattached
= true;
4764 if ( this.clipping
!== clipping
) {
4765 this.clipping
= clipping
;
4767 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4768 // If the clippable container is the root, we have to listen to scroll events and check
4769 // jQuery.scrollTop on the window because of browser inconsistencies
4770 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4771 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4772 this.$clippableScrollableContainer
;
4773 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4774 this.$clippableWindow
= $( this.getElementWindow() )
4775 .on( 'resize', this.onClippableWindowResizeHandler
);
4776 // Initial clip after visible
4779 this.$clippable
.css( {
4787 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4789 this.$clippableScrollableContainer
= null;
4790 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4791 this.$clippableScroller
= null;
4792 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4793 this.$clippableWindow
= null;
4801 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4803 * @return {boolean} Element will be clipped to the visible area
4805 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4806 return this.clipping
;
4810 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4812 * @return {boolean} Part of the element is being clipped
4814 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4815 return this.clippedHorizontally
|| this.clippedVertically
;
4819 * Check if the right of the element is being clipped by the nearest scrollable container.
4821 * @return {boolean} Part of the element is being clipped
4823 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4824 return this.clippedHorizontally
;
4828 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4830 * @return {boolean} Part of the element is being clipped
4832 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4833 return this.clippedVertically
;
4837 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4839 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4840 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4842 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4843 this.idealWidth
= width
;
4844 this.idealHeight
= height
;
4846 if ( !this.clipping
) {
4847 // Update dimensions
4848 this.$clippable
.css( { width
: width
, height
: height
} );
4850 // While clipping, idealWidth and idealHeight are not considered
4854 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4855 * ClippableElement will clip the opposite side when reducing element's width.
4857 * Classes that mix in ClippableElement should override this to return 'right' if their
4858 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
4859 * If your class also mixes in FloatableElement, this is handled automatically.
4861 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4862 * always in pixels, even if they were unset or set to 'auto'.)
4864 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
4866 * @return {string} 'left' or 'right'
4868 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
4869 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
4876 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4877 * ClippableElement will clip the opposite side when reducing element's width.
4879 * Classes that mix in ClippableElement should override this to return 'bottom' if their
4880 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
4881 * If your class also mixes in FloatableElement, this is handled automatically.
4883 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4884 * always in pixels, even if they were unset or set to 'auto'.)
4886 * When in doubt, 'top' is a sane fallback.
4888 * @return {string} 'top' or 'bottom'
4890 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
4891 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
4898 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
4899 * when the element's natural height changes.
4901 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4902 * overlapped by, the visible area of the nearest scrollable container.
4904 * Because calling clip() when the natural height changes isn't always possible, we also set
4905 * max-height when the element isn't being clipped. This means that if the element tries to grow
4906 * beyond the edge, something reasonable will happen before clip() is called.
4910 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
4911 var extraHeight
, extraWidth
, viewportSpacing
,
4912 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
4913 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
4914 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
4915 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
4916 // Extra tolerance so that the sloppy code below doesn't result in results that are off
4917 // by one or two pixels. (And also so that we have space to display drop shadows.)
4918 // Chosen by fair dice roll.
4921 if ( !this.clipping
) {
4922 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
4926 function rectIntersection( a
, b
) {
4928 out
.top
= Math
.max( a
.top
, b
.top
);
4929 out
.left
= Math
.max( a
.left
, b
.left
);
4930 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
4931 out
.right
= Math
.min( a
.right
, b
.right
);
4935 viewportSpacing
= OO
.ui
.getViewportSpacing();
4937 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
4938 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
4939 // Dimensions of the browser window, rather than the element!
4943 right
: document
.documentElement
.clientWidth
,
4944 bottom
: document
.documentElement
.clientHeight
4946 viewportRect
.top
+= viewportSpacing
.top
;
4947 viewportRect
.left
+= viewportSpacing
.left
;
4948 viewportRect
.right
-= viewportSpacing
.right
;
4949 viewportRect
.bottom
-= viewportSpacing
.bottom
;
4951 $viewport
= this.$clippableScrollableContainer
;
4952 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
4953 // Convert into a plain object
4954 viewportRect
= $.extend( {}, viewportRect
);
4957 // Account for scrollbar gutter
4958 direction
= $viewport
.css( 'direction' );
4959 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
4960 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
4961 viewportRect
.bottom
-= horizScrollbarHeight
;
4962 if ( direction
=== 'rtl' ) {
4963 viewportRect
.left
+= vertScrollbarWidth
;
4965 viewportRect
.right
-= vertScrollbarWidth
;
4968 // Add arbitrary tolerance
4969 viewportRect
.top
+= buffer
;
4970 viewportRect
.left
+= buffer
;
4971 viewportRect
.right
-= buffer
;
4972 viewportRect
.bottom
-= buffer
;
4974 $item
= this.$clippableContainer
|| this.$clippable
;
4976 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
4977 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
4979 itemRect
= $item
[ 0 ].getBoundingClientRect();
4980 // Convert into a plain object
4981 itemRect
= $.extend( {}, itemRect
);
4983 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
4984 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
4985 if ( this.getHorizontalAnchorEdge() === 'right' ) {
4986 itemRect
.left
= viewportRect
.left
;
4988 itemRect
.right
= viewportRect
.right
;
4990 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
4991 itemRect
.top
= viewportRect
.top
;
4993 itemRect
.bottom
= viewportRect
.bottom
;
4996 availableRect
= rectIntersection( viewportRect
, itemRect
);
4998 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
4999 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5000 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5001 desiredWidth
= Math
.min( desiredWidth
,
5002 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5003 desiredHeight
= Math
.min( desiredHeight
,
5004 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5005 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5006 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5007 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5008 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5009 clipWidth
= allotedWidth
< naturalWidth
;
5010 clipHeight
= allotedHeight
< naturalHeight
;
5013 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5014 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5015 this.$clippable
.css( 'overflowX', 'scroll' );
5016 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5017 this.$clippable
.css( {
5018 width
: Math
.max( 0, allotedWidth
),
5022 this.$clippable
.css( {
5024 width
: this.idealWidth
|| '',
5025 maxWidth
: Math
.max( 0, allotedWidth
)
5029 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5030 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5031 this.$clippable
.css( 'overflowY', 'scroll' );
5032 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5033 this.$clippable
.css( {
5034 height
: Math
.max( 0, allotedHeight
),
5038 this.$clippable
.css( {
5040 height
: this.idealHeight
|| '',
5041 maxHeight
: Math
.max( 0, allotedHeight
)
5045 // If we stopped clipping in at least one of the dimensions
5046 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5047 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5050 this.clippedHorizontally
= clipWidth
;
5051 this.clippedVertically
= clipHeight
;
5057 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5058 * By default, each popup has an anchor that points toward its origin.
5059 * Please see the [OOUI documentation on Mediawiki] [1] for more information and examples.
5061 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5064 * // A popup widget.
5065 * var popup = new OO.ui.PopupWidget( {
5066 * $content: $( '<p>Hi there!</p>' ),
5071 * $( 'body' ).append( popup.$element );
5072 * // To display the popup, toggle the visibility to 'true'.
5073 * popup.toggle( true );
5075 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5078 * @extends OO.ui.Widget
5079 * @mixins OO.ui.mixin.LabelElement
5080 * @mixins OO.ui.mixin.ClippableElement
5081 * @mixins OO.ui.mixin.FloatableElement
5084 * @param {Object} [config] Configuration options
5085 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5086 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5087 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5088 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5089 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5090 * of $floatableContainer
5091 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5092 * of $floatableContainer
5093 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5094 * endwards (right/left) to the vertical center of $floatableContainer
5095 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5096 * startwards (left/right) to the vertical center of $floatableContainer
5097 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5098 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
5099 * as possible while still keeping the anchor within the popup;
5100 * if position is before/after, move the popup as far downwards as possible.
5101 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
5102 * as possible while still keeping the anchor within the popup;
5103 * if position in before/after, move the popup as far upwards as possible.
5104 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
5105 * of the popup with the center of $floatableContainer.
5106 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5107 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5108 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5109 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5110 * desired direction to display the popup without clipping
5111 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5112 * See the [OOUI docs on MediaWiki][3] for an example.
5113 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5114 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
5115 * @cfg {jQuery} [$content] Content to append to the popup's body
5116 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5117 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5118 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5119 * This config option is only relevant if #autoClose is set to `true`. See the [OOUI documentation on MediaWiki][2]
5121 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5122 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5124 * @cfg {boolean} [padded=false] Add padding to the popup's body
5126 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5127 // Configuration initialization
5128 config
= config
|| {};
5130 // Parent constructor
5131 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5133 // Properties (must be set before ClippableElement constructor call)
5134 this.$body
= $( '<div>' );
5135 this.$popup
= $( '<div>' );
5137 // Mixin constructors
5138 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5139 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
5140 $clippable
: this.$body
,
5141 $clippableContainer
: this.$popup
5143 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5146 this.$anchor
= $( '<div>' );
5147 // If undefined, will be computed lazily in computePosition()
5148 this.$container
= config
.$container
;
5149 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5150 this.autoClose
= !!config
.autoClose
;
5151 this.transitionTimeout
= null;
5152 this.anchored
= false;
5153 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
5154 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5157 this.setSize( config
.width
, config
.height
);
5158 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5159 this.setAlignment( config
.align
|| 'center' );
5160 this.setPosition( config
.position
|| 'below' );
5161 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5162 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5163 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5164 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5166 .addClass( 'oo-ui-popupWidget-popup' )
5167 .append( this.$body
);
5169 .addClass( 'oo-ui-popupWidget' )
5170 .append( this.$popup
, this.$anchor
);
5171 // Move content, which was added to #$element by OO.ui.Widget, to the body
5172 // FIXME This is gross, we should use '$body' or something for the config
5173 if ( config
.$content
instanceof jQuery
) {
5174 this.$body
.append( config
.$content
);
5177 if ( config
.padded
) {
5178 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5181 if ( config
.head
) {
5182 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
5183 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
5184 this.$head
= $( '<div>' )
5185 .addClass( 'oo-ui-popupWidget-head' )
5186 .append( this.$label
, this.closeButton
.$element
);
5187 this.$popup
.prepend( this.$head
);
5190 if ( config
.$footer
) {
5191 this.$footer
= $( '<div>' )
5192 .addClass( 'oo-ui-popupWidget-footer' )
5193 .append( config
.$footer
);
5194 this.$popup
.append( this.$footer
);
5197 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5198 // that reference properties not initialized at that time of parent class construction
5199 // TODO: Find a better way to handle post-constructor setup
5200 this.visible
= false;
5201 this.$element
.addClass( 'oo-ui-element-hidden' );
5206 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5207 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5208 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5209 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5216 * The popup is ready: it is visible and has been positioned and clipped.
5222 * Handles mouse down events.
5225 * @param {MouseEvent} e Mouse down event
5227 OO
.ui
.PopupWidget
.prototype.onMouseDown = function ( e
) {
5230 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5232 this.toggle( false );
5237 * Bind mouse down listener.
5241 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
5242 // Capture clicks outside popup
5243 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler
, true );
5244 // We add 'click' event because iOS safari needs to respond to this event.
5245 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5246 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5247 // of occasionally not emitting 'click' properly, that event seems to be the standard
5248 // that it should be emitting, so we add it to this and will operate the event handler
5249 // on whichever of these events was triggered first
5250 this.getElementDocument().addEventListener( 'click', this.onMouseDownHandler
, true );
5254 * Handles close button click events.
5258 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5259 if ( this.isVisible() ) {
5260 this.toggle( false );
5265 * Unbind mouse down listener.
5269 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
5270 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler
, true );
5271 this.getElementDocument().removeEventListener( 'click', this.onMouseDownHandler
, true );
5275 * Handles key down events.
5278 * @param {KeyboardEvent} e Key down event
5280 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5282 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5285 this.toggle( false );
5287 e
.stopPropagation();
5292 * Bind key down listener.
5296 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
5297 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5301 * Unbind key down listener.
5305 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
5306 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5310 * Show, hide, or toggle the visibility of the anchor.
5312 * @param {boolean} [show] Show anchor, omit to toggle
5314 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5315 show
= show
=== undefined ? !this.anchored
: !!show
;
5317 if ( this.anchored
!== show
) {
5319 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5320 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5322 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5323 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5325 this.anchored
= show
;
5330 * Change which edge the anchor appears on.
5332 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5334 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5335 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5336 throw new Error( 'Invalid value for edge: ' + edge
);
5338 if ( this.anchorEdge
!== null ) {
5339 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5341 this.anchorEdge
= edge
;
5342 if ( this.anchored
) {
5343 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5348 * Check if the anchor is visible.
5350 * @return {boolean} Anchor is visible
5352 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5353 return this.anchored
;
5357 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5358 * `.toggle( true )` after its #$element is attached to the DOM.
5360 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5361 * it in the right place and with the right dimensions only work correctly while it is attached.
5362 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5363 * strictly enforced, so currently it only generates a warning in the browser console.
5368 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5369 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5370 show
= show
=== undefined ? !this.isVisible() : !!show
;
5372 change
= show
!== this.isVisible();
5374 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5375 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5376 this.warnedUnattached
= true;
5378 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5379 // Fall back to the parent node if the floatableContainer is not set
5380 this.setFloatableContainer( this.$element
.parent() );
5383 if ( change
&& show
&& this.autoFlip
) {
5384 // Reset auto-flipping before showing the popup again. It's possible we no longer need to flip
5385 // (e.g. if the user scrolled).
5386 this.isAutoFlipped
= false;
5390 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5393 this.togglePositioning( show
&& !!this.$floatableContainer
);
5396 if ( this.autoClose
) {
5397 this.bindMouseDownListener();
5398 this.bindKeyDownListener();
5400 this.updateDimensions();
5401 this.toggleClipping( true );
5403 if ( this.autoFlip
) {
5404 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5405 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5406 // If opening the popup in the normal direction causes it to be clipped, open
5407 // in the opposite one instead
5408 normalHeight
= this.$element
.height();
5409 this.isAutoFlipped
= !this.isAutoFlipped
;
5411 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5412 // If that also causes it to be clipped, open in whichever direction
5413 // we have more space
5414 oppositeHeight
= this.$element
.height();
5415 if ( oppositeHeight
< normalHeight
) {
5416 this.isAutoFlipped
= !this.isAutoFlipped
;
5422 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5423 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5424 // If opening the popup in the normal direction causes it to be clipped, open
5425 // in the opposite one instead
5426 normalWidth
= this.$element
.width();
5427 this.isAutoFlipped
= !this.isAutoFlipped
;
5428 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5429 // which causes positioning to be off. Toggle clipping back and fort to work around.
5430 this.toggleClipping( false );
5432 this.toggleClipping( true );
5433 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5434 // If that also causes it to be clipped, open in whichever direction
5435 // we have more space
5436 oppositeWidth
= this.$element
.width();
5437 if ( oppositeWidth
< normalWidth
) {
5438 this.isAutoFlipped
= !this.isAutoFlipped
;
5439 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5440 // which causes positioning to be off. Toggle clipping back and fort to work around.
5441 this.toggleClipping( false );
5443 this.toggleClipping( true );
5450 this.emit( 'ready' );
5452 this.toggleClipping( false );
5453 if ( this.autoClose
) {
5454 this.unbindMouseDownListener();
5455 this.unbindKeyDownListener();
5464 * Set the size of the popup.
5466 * Changing the size may also change the popup's position depending on the alignment.
5468 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5469 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5470 * @param {boolean} [transition=false] Use a smooth transition
5473 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5474 this.width
= width
!== undefined ? width
: 320;
5475 this.height
= height
!== undefined ? height
: null;
5476 if ( this.isVisible() ) {
5477 this.updateDimensions( transition
);
5482 * Update the size and position.
5484 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5485 * be called automatically.
5487 * @param {boolean} [transition=false] Use a smooth transition
5490 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5493 // Prevent transition from being interrupted
5494 clearTimeout( this.transitionTimeout
);
5496 // Enable transition
5497 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5503 // Prevent transitioning after transition is complete
5504 this.transitionTimeout
= setTimeout( function () {
5505 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5508 // Prevent transitioning immediately
5509 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5516 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5517 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
, anchorPos
,
5518 anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
, floatablePos
,
5519 offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5521 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5522 popupPositionOppositeMap
= {
5530 'force-left': 'backwards',
5531 'force-right': 'forwards'
5534 'force-left': 'forwards',
5535 'force-right': 'backwards'
5547 backwards
: this.anchored
? 'before' : 'end'
5555 if ( !this.$container
) {
5556 // Lazy-initialize $container if not specified in constructor
5557 this.$container
= $( this.getClosestScrollableElementContainer() );
5559 direction
= this.$container
.css( 'direction' );
5561 // Set height and width before we do anything else, since it might cause our measurements
5562 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5564 width
: this.width
!== null ? this.width
: 'auto',
5565 height
: this.height
!== null ? this.height
: 'auto'
5568 align
= alignMap
[ direction
][ this.align
] || this.align
;
5569 popupPosition
= this.popupPosition
;
5570 if ( this.isAutoFlipped
) {
5571 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5574 // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5575 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5576 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5577 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5578 near
= vertical
? 'top' : 'left';
5579 far
= vertical
? 'bottom' : 'right';
5580 sizeProp
= vertical
? 'Height' : 'Width';
5581 popupSize
= vertical
? ( this.height
|| this.$popup
.height() ) : ( this.width
|| this.$popup
.width() );
5583 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5584 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5585 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5588 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5589 // Find out which property FloatableElement used for positioning, and adjust that value
5590 positionProp
= vertical
?
5591 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5592 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5594 // Figure out where the near and far edges of the popup and $floatableContainer are
5595 floatablePos
= this.$floatableContainer
.offset();
5596 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5597 // Measure where the offsetParent is and compute our position based on that and parentPosition
5598 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5599 { top
: 0, left
: 0 } :
5600 this.$element
.offsetParent().offset();
5602 if ( positionProp
=== near
) {
5603 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5604 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5606 popupPos
[ far
] = offsetParentPos
[ near
] +
5607 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5608 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5611 if ( this.anchored
) {
5612 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5613 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5614 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5616 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5617 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5618 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5619 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5620 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5621 // Not enough space for the anchor on the start side; pull the popup startwards
5622 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5623 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5624 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5625 // Not enough space for the anchor on the end side; pull the popup endwards
5626 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5627 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5629 positionAdjustment
= 0;
5632 positionAdjustment
= 0;
5635 // Check if the popup will go beyond the edge of this.$container
5636 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5637 { top
: 0, left
: 0 } :
5638 this.$container
.offset();
5639 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5640 if ( this.$container
[ 0 ] === document
.documentElement
) {
5641 viewportSpacing
= OO
.ui
.getViewportSpacing();
5642 containerPos
[ near
] += viewportSpacing
[ near
];
5643 containerPos
[ far
] -= viewportSpacing
[ far
];
5645 // Take into account how much the popup will move because of the adjustments we're going to make
5646 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5647 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5648 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5649 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5650 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5651 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5652 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5653 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5654 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5655 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5658 if ( this.anchored
) {
5659 // Adjust anchorOffset for positionAdjustment
5660 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5662 // Position the anchor
5663 anchorCss
[ start
] = anchorOffset
;
5664 this.$anchor
.css( anchorCss
);
5667 // Move the popup if needed
5668 parentPosition
[ positionProp
] += positionAdjustment
;
5670 return parentPosition
;
5674 * Set popup alignment
5676 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5677 * `backwards` or `forwards`.
5679 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5680 // Validate alignment
5681 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5684 this.align
= 'center';
5690 * Get popup alignment
5692 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5693 * `backwards` or `forwards`.
5695 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5700 * Change the positioning of the popup.
5702 * @param {string} position 'above', 'below', 'before' or 'after'
5704 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5705 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5708 this.popupPosition
= position
;
5713 * Get popup positioning.
5715 * @return {string} 'above', 'below', 'before' or 'after'
5717 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5718 return this.popupPosition
;
5722 * Set popup auto-flipping.
5724 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5725 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5726 * desired direction to display the popup without clipping
5728 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5729 autoFlip
= !!autoFlip
;
5731 if ( this.autoFlip
!== autoFlip
) {
5732 this.autoFlip
= autoFlip
;
5737 * Set which elements will not close the popup when clicked.
5739 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
5741 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
5743 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
5744 this.$autoCloseIgnore
= $autoCloseIgnore
;
5748 * Get an ID of the body element, this can be used as the
5749 * `aria-describedby` attribute for an input field.
5751 * @return {string} The ID of the body element
5753 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5754 var id
= this.$body
.attr( 'id' );
5755 if ( id
=== undefined ) {
5756 id
= OO
.ui
.generateElementId();
5757 this.$body
.attr( 'id', id
);
5763 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5764 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5765 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5766 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5772 * @param {Object} [config] Configuration options
5773 * @cfg {Object} [popup] Configuration to pass to popup
5774 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5776 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5777 // Configuration initialization
5778 config
= config
|| {};
5781 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5784 $floatableContainer
: this.$element
5788 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5798 * @return {OO.ui.PopupWidget} Popup widget
5800 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5805 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5806 * which is used to display additional information or options.
5809 * // Example of a popup button.
5810 * var popupButton = new OO.ui.PopupButtonWidget( {
5811 * label: 'Popup button with options',
5814 * $content: $( '<p>Additional options here.</p>' ),
5816 * align: 'force-left'
5819 * // Append the button to the DOM.
5820 * $( 'body' ).append( popupButton.$element );
5823 * @extends OO.ui.ButtonWidget
5824 * @mixins OO.ui.mixin.PopupElement
5827 * @param {Object} [config] Configuration options
5828 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
5829 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
5830 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
5831 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
5833 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
5834 // Configuration initialization
5835 config
= config
|| {};
5837 // Parent constructor
5838 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
5840 // Mixin constructors
5841 OO
.ui
.mixin
.PopupElement
.call( this, config
);
5844 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
5847 this.connect( this, { click
: 'onAction' } );
5851 .addClass( 'oo-ui-popupButtonWidget' );
5853 .addClass( 'oo-ui-popupButtonWidget-popup' )
5854 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
5855 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
5856 this.$overlay
.append( this.popup
.$element
);
5861 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
5862 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
5867 * Handle the button action being triggered.
5871 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
5872 this.popup
.toggle();
5876 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
5878 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
5883 * @mixins OO.ui.mixin.GroupElement
5886 * @param {Object} [config] Configuration options
5888 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
5889 // Mixin constructors
5890 OO
.ui
.mixin
.GroupElement
.call( this, config
);
5895 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
5900 * Set the disabled state of the widget.
5902 * This will also update the disabled state of child widgets.
5904 * @param {boolean} disabled Disable widget
5907 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
5911 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
5912 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
5914 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
5916 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
5917 this.items
[ i
].updateDisabled();
5925 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
5927 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
5928 * allows bidirectional communication.
5930 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
5938 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
5945 * Check if widget is disabled.
5947 * Checks parent if present, making disabled state inheritable.
5949 * @return {boolean} Widget is disabled
5951 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
5952 return this.disabled
||
5953 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
5957 * Set group element is in.
5959 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
5962 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
5964 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
5965 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
5967 // Initialize item disabled states
5968 this.updateDisabled();
5974 * OptionWidgets are special elements that can be selected and configured with data. The
5975 * data is often unique for each option, but it does not have to be. OptionWidgets are used
5976 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
5977 * and examples, please see the [OOUI documentation on MediaWiki][1].
5979 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
5982 * @extends OO.ui.Widget
5983 * @mixins OO.ui.mixin.ItemWidget
5984 * @mixins OO.ui.mixin.LabelElement
5985 * @mixins OO.ui.mixin.FlaggedElement
5986 * @mixins OO.ui.mixin.AccessKeyedElement
5989 * @param {Object} [config] Configuration options
5991 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
5992 // Configuration initialization
5993 config
= config
|| {};
5995 // Parent constructor
5996 OO
.ui
.OptionWidget
.parent
.call( this, config
);
5998 // Mixin constructors
5999 OO
.ui
.mixin
.ItemWidget
.call( this );
6000 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6001 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6002 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6005 this.selected
= false;
6006 this.highlighted
= false;
6007 this.pressed
= false;
6011 .data( 'oo-ui-optionWidget', this )
6012 // Allow programmatic focussing (and by accesskey), but not tabbing
6013 .attr( 'tabindex', '-1' )
6014 .attr( 'role', 'option' )
6015 .attr( 'aria-selected', 'false' )
6016 .addClass( 'oo-ui-optionWidget' )
6017 .append( this.$label
);
6022 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6023 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6024 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6025 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6026 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6028 /* Static Properties */
6031 * Whether this option can be selected. See #setSelected.
6035 * @property {boolean}
6037 OO
.ui
.OptionWidget
.static.selectable
= true;
6040 * Whether this option can be highlighted. See #setHighlighted.
6044 * @property {boolean}
6046 OO
.ui
.OptionWidget
.static.highlightable
= true;
6049 * Whether this option can be pressed. See #setPressed.
6053 * @property {boolean}
6055 OO
.ui
.OptionWidget
.static.pressable
= true;
6058 * Whether this option will be scrolled into view when it is selected.
6062 * @property {boolean}
6064 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6069 * Check if the option can be selected.
6071 * @return {boolean} Item is selectable
6073 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6074 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6078 * Check if the option can be highlighted. A highlight indicates that the option
6079 * may be selected when a user presses enter or clicks. Disabled items cannot
6082 * @return {boolean} Item is highlightable
6084 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6085 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6089 * Check if the option can be pressed. The pressed state occurs when a user mouses
6090 * down on an item, but has not yet let go of the mouse.
6092 * @return {boolean} Item is pressable
6094 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6095 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6099 * Check if the option is selected.
6101 * @return {boolean} Item is selected
6103 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6104 return this.selected
;
6108 * Check if the option is highlighted. A highlight indicates that the
6109 * item may be selected when a user presses enter or clicks.
6111 * @return {boolean} Item is highlighted
6113 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6114 return this.highlighted
;
6118 * Check if the option is pressed. The pressed state occurs when a user mouses
6119 * down on an item, but has not yet let go of the mouse. The item may appear
6120 * selected, but it will not be selected until the user releases the mouse.
6122 * @return {boolean} Item is pressed
6124 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6125 return this.pressed
;
6129 * Set the option’s selected state. In general, all modifications to the selection
6130 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6131 * method instead of this method.
6133 * @param {boolean} [state=false] Select option
6136 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6137 if ( this.constructor.static.selectable
) {
6138 this.selected
= !!state
;
6140 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6141 .attr( 'aria-selected', state
.toString() );
6142 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6143 this.scrollElementIntoView();
6145 this.updateThemeClasses();
6151 * Set the option’s highlighted state. In general, all programmatic
6152 * modifications to the highlight should be handled by the
6153 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6154 * method instead of this method.
6156 * @param {boolean} [state=false] Highlight option
6159 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6160 if ( this.constructor.static.highlightable
) {
6161 this.highlighted
= !!state
;
6162 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6163 this.updateThemeClasses();
6169 * Set the option’s pressed state. In general, all
6170 * programmatic modifications to the pressed state should be handled by the
6171 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6172 * method instead of this method.
6174 * @param {boolean} [state=false] Press option
6177 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6178 if ( this.constructor.static.pressable
) {
6179 this.pressed
= !!state
;
6180 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6181 this.updateThemeClasses();
6187 * Get text to match search strings against.
6189 * The default implementation returns the label text, but subclasses
6190 * can override this to provide more complex behavior.
6192 * @return {string|boolean} String to match search string against
6194 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6195 var label
= this.getLabel();
6196 return typeof label
=== 'string' ? label
: this.$label
.text();
6200 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6201 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6202 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6205 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
6206 * information, please see the [OOUI documentation on MediaWiki][1].
6209 * // Example of a select widget with three options
6210 * var select = new OO.ui.SelectWidget( {
6212 * new OO.ui.OptionWidget( {
6214 * label: 'Option One',
6216 * new OO.ui.OptionWidget( {
6218 * label: 'Option Two',
6220 * new OO.ui.OptionWidget( {
6222 * label: 'Option Three',
6226 * $( 'body' ).append( select.$element );
6228 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6232 * @extends OO.ui.Widget
6233 * @mixins OO.ui.mixin.GroupWidget
6236 * @param {Object} [config] Configuration options
6237 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6238 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6239 * the [OOUI documentation on MediaWiki] [2] for examples.
6240 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6242 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6243 // Configuration initialization
6244 config
= config
|| {};
6246 // Parent constructor
6247 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6249 // Mixin constructors
6250 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
6253 this.pressed
= false;
6254 this.selecting
= null;
6255 this.onMouseUpHandler
= this.onMouseUp
.bind( this );
6256 this.onMouseMoveHandler
= this.onMouseMove
.bind( this );
6257 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
6258 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
6259 this.keyPressBuffer
= '';
6260 this.keyPressBufferTimer
= null;
6261 this.blockMouseOverEvents
= 0;
6264 this.connect( this, {
6268 focusin
: this.onFocus
.bind( this ),
6269 mousedown
: this.onMouseDown
.bind( this ),
6270 mouseover
: this.onMouseOver
.bind( this ),
6271 mouseleave
: this.onMouseLeave
.bind( this )
6276 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
6277 .attr( 'role', 'listbox' );
6278 this.setFocusOwner( this.$element
);
6279 if ( Array
.isArray( config
.items
) ) {
6280 this.addItems( config
.items
);
6286 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6287 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6294 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6296 * @param {OO.ui.OptionWidget|null} item Highlighted item
6302 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6303 * pressed state of an option.
6305 * @param {OO.ui.OptionWidget|null} item Pressed item
6311 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
6313 * @param {OO.ui.OptionWidget|null} item Selected item
6318 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6319 * @param {OO.ui.OptionWidget} item Chosen item
6325 * An `add` event is emitted when options are added to the select with the #addItems method.
6327 * @param {OO.ui.OptionWidget[]} items Added items
6328 * @param {number} index Index of insertion point
6334 * A `remove` event is emitted when options are removed from the select with the #clearItems
6335 * or #removeItems methods.
6337 * @param {OO.ui.OptionWidget[]} items Removed items
6343 * Handle focus events
6346 * @param {jQuery.Event} event
6348 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6350 if ( event
.target
=== this.$element
[ 0 ] ) {
6351 // This widget was focussed, e.g. by the user tabbing to it.
6352 // The styles for focus state depend on one of the items being selected.
6353 if ( !this.findSelectedItem() ) {
6354 item
= this.findFirstSelectableItem();
6357 if ( event
.target
.tabIndex
=== -1 ) {
6358 // One of the options got focussed (and the event bubbled up here).
6359 // They can't be tabbed to, but they can be activated using accesskeys.
6360 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6361 item
= this.findTargetItem( event
);
6363 // There is something actually user-focusable in one of the labels of the options, and the
6364 // user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change the focus).
6370 if ( item
.constructor.static.highlightable
) {
6371 this.highlightItem( item
);
6373 this.selectItem( item
);
6377 if ( event
.target
!== this.$element
[ 0 ] ) {
6378 this.$focusOwner
.focus();
6383 * Handle mouse down events.
6386 * @param {jQuery.Event} e Mouse down event
6388 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6391 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6392 this.togglePressed( true );
6393 item
= this.findTargetItem( e
);
6394 if ( item
&& item
.isSelectable() ) {
6395 this.pressItem( item
);
6396 this.selecting
= item
;
6397 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler
, true );
6398 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6405 * Handle mouse up events.
6408 * @param {MouseEvent} e Mouse up event
6410 OO
.ui
.SelectWidget
.prototype.onMouseUp = function ( e
) {
6413 this.togglePressed( false );
6414 if ( !this.selecting
) {
6415 item
= this.findTargetItem( e
);
6416 if ( item
&& item
.isSelectable() ) {
6417 this.selecting
= item
;
6420 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6421 this.pressItem( null );
6422 this.chooseItem( this.selecting
);
6423 this.selecting
= null;
6426 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler
, true );
6427 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler
, true );
6433 * Handle mouse move events.
6436 * @param {MouseEvent} e Mouse move event
6438 OO
.ui
.SelectWidget
.prototype.onMouseMove = function ( e
) {
6441 if ( !this.isDisabled() && this.pressed
) {
6442 item
= this.findTargetItem( e
);
6443 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6444 this.pressItem( item
);
6445 this.selecting
= item
;
6451 * Handle mouse over events.
6454 * @param {jQuery.Event} e Mouse over event
6456 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6458 if ( this.blockMouseOverEvents
) {
6461 if ( !this.isDisabled() ) {
6462 item
= this.findTargetItem( e
);
6463 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6469 * Handle mouse leave events.
6472 * @param {jQuery.Event} e Mouse over event
6474 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6475 if ( !this.isDisabled() ) {
6476 this.highlightItem( null );
6482 * Handle key down events.
6485 * @param {KeyboardEvent} e Key down event
6487 OO
.ui
.SelectWidget
.prototype.onKeyDown = function ( e
) {
6490 currentItem
= this.findHighlightedItem() || this.findSelectedItem();
6492 if ( !this.isDisabled() && this.isVisible() ) {
6493 switch ( e
.keyCode
) {
6494 case OO
.ui
.Keys
.ENTER
:
6495 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6496 // Was only highlighted, now let's select it. No-op if already selected.
6497 this.chooseItem( currentItem
);
6502 case OO
.ui
.Keys
.LEFT
:
6503 this.clearKeyPressBuffer();
6504 nextItem
= this.findRelativeSelectableItem( currentItem
, -1 );
6507 case OO
.ui
.Keys
.DOWN
:
6508 case OO
.ui
.Keys
.RIGHT
:
6509 this.clearKeyPressBuffer();
6510 nextItem
= this.findRelativeSelectableItem( currentItem
, 1 );
6513 case OO
.ui
.Keys
.ESCAPE
:
6514 case OO
.ui
.Keys
.TAB
:
6515 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6516 currentItem
.setHighlighted( false );
6518 this.unbindKeyDownListener();
6519 this.unbindKeyPressListener();
6520 // Don't prevent tabbing away / defocusing
6526 if ( nextItem
.constructor.static.highlightable
) {
6527 this.highlightItem( nextItem
);
6529 this.chooseItem( nextItem
);
6531 this.scrollItemIntoView( nextItem
);
6536 e
.stopPropagation();
6542 * Bind key down listener.
6546 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6547 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler
, true );
6551 * Unbind key down listener.
6555 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6556 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler
, true );
6560 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6562 * @param {OO.ui.OptionWidget} item Item to scroll into view
6564 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6566 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6567 // and around 100-150 ms after it is finished.
6568 this.blockMouseOverEvents
++;
6569 item
.scrollElementIntoView().done( function () {
6570 setTimeout( function () {
6571 widget
.blockMouseOverEvents
--;
6577 * Clear the key-press buffer
6581 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6582 if ( this.keyPressBufferTimer
) {
6583 clearTimeout( this.keyPressBufferTimer
);
6584 this.keyPressBufferTimer
= null;
6586 this.keyPressBuffer
= '';
6590 * Handle key press events.
6593 * @param {KeyboardEvent} e Key press event
6595 OO
.ui
.SelectWidget
.prototype.onKeyPress = function ( e
) {
6596 var c
, filter
, item
;
6598 if ( !e
.charCode
) {
6599 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6600 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6605 if ( String
.fromCodePoint
) {
6606 c
= String
.fromCodePoint( e
.charCode
);
6608 c
= String
.fromCharCode( e
.charCode
);
6611 if ( this.keyPressBufferTimer
) {
6612 clearTimeout( this.keyPressBufferTimer
);
6614 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6616 item
= this.findHighlightedItem() || this.findSelectedItem();
6618 if ( this.keyPressBuffer
=== c
) {
6619 // Common (if weird) special case: typing "xxxx" will cycle through all
6620 // the items beginning with "x".
6622 item
= this.findRelativeSelectableItem( item
, 1 );
6625 this.keyPressBuffer
+= c
;
6628 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6629 if ( !item
|| !filter( item
) ) {
6630 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6633 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6634 this.highlightItem( item
);
6636 this.chooseItem( item
);
6638 this.scrollItemIntoView( item
);
6642 e
.stopPropagation();
6646 * Get a matcher for the specific string
6649 * @param {string} s String to match against items
6650 * @param {boolean} [exact=false] Only accept exact matches
6651 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6653 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
6656 if ( s
.normalize
) {
6659 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
6660 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6664 re
= new RegExp( re
, 'i' );
6665 return function ( item
) {
6666 var matchText
= item
.getMatchText();
6667 if ( matchText
.normalize
) {
6668 matchText
= matchText
.normalize();
6670 return re
.test( matchText
);
6675 * Bind key press listener.
6679 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
6680 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler
, true );
6684 * Unbind key down listener.
6686 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6691 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
6692 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler
, true );
6693 this.clearKeyPressBuffer();
6697 * Visibility change handler
6700 * @param {boolean} visible
6702 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6704 this.clearKeyPressBuffer();
6709 * Get the closest item to a jQuery.Event.
6712 * @param {jQuery.Event} e
6713 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6715 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6716 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6717 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6720 return $option
.data( 'oo-ui-optionWidget' ) || null;
6724 * Find selected item.
6726 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6728 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
6731 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6732 if ( this.items
[ i
].isSelected() ) {
6733 return this.items
[ i
];
6740 * Find highlighted item.
6742 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6744 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6747 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6748 if ( this.items
[ i
].isHighlighted() ) {
6749 return this.items
[ i
];
6756 * Toggle pressed state.
6758 * Press is a state that occurs when a user mouses down on an item, but
6759 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
6760 * until the user releases the mouse.
6762 * @param {boolean} pressed An option is being pressed
6764 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
6765 if ( pressed
=== undefined ) {
6766 pressed
= !this.pressed
;
6768 if ( pressed
!== this.pressed
) {
6770 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
6771 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
6772 this.pressed
= pressed
;
6777 * Highlight an option. If the `item` param is omitted, no options will be highlighted
6778 * and any existing highlight will be removed. The highlight is mutually exclusive.
6780 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
6784 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
6785 var i
, len
, highlighted
,
6788 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6789 highlighted
= this.items
[ i
] === item
;
6790 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
6791 this.items
[ i
].setHighlighted( highlighted
);
6797 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6799 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6801 this.emit( 'highlight', item
);
6808 * Fetch an item by its label.
6810 * @param {string} label Label of the item to select.
6811 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6812 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
6814 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
6816 len
= this.items
.length
,
6817 filter
= this.getItemMatcher( label
, true );
6819 for ( i
= 0; i
< len
; i
++ ) {
6820 item
= this.items
[ i
];
6821 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6828 filter
= this.getItemMatcher( label
, false );
6829 for ( i
= 0; i
< len
; i
++ ) {
6830 item
= this.items
[ i
];
6831 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
6847 * Programmatically select an option by its label. If the item does not exist,
6848 * all options will be deselected.
6850 * @param {string} [label] Label of the item to select.
6851 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
6855 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
6856 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
6857 if ( label
=== undefined || !itemFromLabel
) {
6858 return this.selectItem();
6860 return this.selectItem( itemFromLabel
);
6864 * Programmatically select an option by its data. If the `data` parameter is omitted,
6865 * or if the item does not exist, all options will be deselected.
6867 * @param {Object|string} [data] Value of the item to select, omit to deselect all
6871 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
6872 var itemFromData
= this.findItemFromData( data
);
6873 if ( data
=== undefined || !itemFromData
) {
6874 return this.selectItem();
6876 return this.selectItem( itemFromData
);
6880 * Programmatically select an option by its reference. If the `item` parameter is omitted,
6881 * all options will be deselected.
6883 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
6887 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
6888 var i
, len
, selected
,
6891 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6892 selected
= this.items
[ i
] === item
;
6893 if ( this.items
[ i
].isSelected() !== selected
) {
6894 this.items
[ i
].setSelected( selected
);
6899 if ( item
&& !item
.constructor.static.highlightable
) {
6901 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
6903 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
6906 this.emit( 'select', item
);
6915 * Press is a state that occurs when a user mouses down on an item, but has not
6916 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
6917 * releases the mouse.
6919 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
6923 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
6924 var i
, len
, pressed
,
6927 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6928 pressed
= this.items
[ i
] === item
;
6929 if ( this.items
[ i
].isPressed() !== pressed
) {
6930 this.items
[ i
].setPressed( pressed
);
6935 this.emit( 'press', item
);
6944 * Note that ‘choose’ should never be modified programmatically. A user can choose
6945 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
6946 * use the #selectItem method.
6948 * This method is identical to #selectItem, but may vary in subclasses that take additional action
6949 * when users choose an item with the keyboard or mouse.
6951 * @param {OO.ui.OptionWidget} item Item to choose
6955 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
6957 this.selectItem( item
);
6958 this.emit( 'choose', item
);
6965 * Find an option by its position relative to the specified item (or to the start of the option array,
6966 * if item is `null`). The direction in which to search through the option array is specified with a
6967 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
6968 * `null` if there are no options in the array.
6970 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
6971 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
6972 * @param {Function} [filter] Only consider items for which this function returns
6973 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
6974 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
6976 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
6977 var currentIndex
, nextIndex
, i
,
6978 increase
= direction
> 0 ? 1 : -1,
6979 len
= this.items
.length
;
6981 if ( item
instanceof OO
.ui
.OptionWidget
) {
6982 currentIndex
= this.items
.indexOf( item
);
6983 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
6985 // If no item is selected and moving forward, start at the beginning.
6986 // If moving backward, start at the end.
6987 nextIndex
= direction
> 0 ? 0 : len
- 1;
6990 for ( i
= 0; i
< len
; i
++ ) {
6991 item
= this.items
[ nextIndex
];
6993 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
6994 ( !filter
|| filter( item
) )
6998 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7004 * Find the next selectable item or `null` if there are no selectable items.
7005 * Disabled options and menu-section markers and breaks are not selectable.
7007 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7009 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7010 return this.findRelativeSelectableItem( null, 1 );
7014 * Add an array of options to the select. Optionally, an index number can be used to
7015 * specify an insertion point.
7017 * @param {OO.ui.OptionWidget[]} items Items to add
7018 * @param {number} [index] Index to insert items after
7022 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7024 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7026 // Always provide an index, even if it was omitted
7027 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7033 * Remove the specified array of options from the select. Options will be detached
7034 * from the DOM, not removed, so they can be reused later. To remove all options from
7035 * the select, you may wish to use the #clearItems method instead.
7037 * @param {OO.ui.OptionWidget[]} items Items to remove
7041 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7044 // Deselect items being removed
7045 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7047 if ( item
.isSelected() ) {
7048 this.selectItem( null );
7053 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7055 this.emit( 'remove', items
);
7061 * Clear all options from the select. Options will be detached from the DOM, not removed,
7062 * so that they can be reused later. To remove a subset of options from the select, use
7063 * the #removeItems method.
7068 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7069 var items
= this.items
.slice();
7072 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7075 this.selectItem( null );
7077 this.emit( 'remove', items
);
7083 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7085 * Currently this is just used to set `aria-activedescendant` on it.
7088 * @param {jQuery} $focusOwner
7090 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7091 this.$focusOwner
= $focusOwner
;
7095 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7096 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
7097 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7098 * options. For more information about options and selects, please see the
7099 * [OOUI documentation on MediaWiki][1].
7102 * // Decorated options in a select widget
7103 * var select = new OO.ui.SelectWidget( {
7105 * new OO.ui.DecoratedOptionWidget( {
7107 * label: 'Option with icon',
7110 * new OO.ui.DecoratedOptionWidget( {
7112 * label: 'Option with indicator',
7117 * $( 'body' ).append( select.$element );
7119 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7122 * @extends OO.ui.OptionWidget
7123 * @mixins OO.ui.mixin.IconElement
7124 * @mixins OO.ui.mixin.IndicatorElement
7127 * @param {Object} [config] Configuration options
7129 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7130 // Parent constructor
7131 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7133 // Mixin constructors
7134 OO
.ui
.mixin
.IconElement
.call( this, config
);
7135 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7139 .addClass( 'oo-ui-decoratedOptionWidget' )
7140 .prepend( this.$icon
)
7141 .append( this.$indicator
);
7146 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7147 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7148 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7151 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7152 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7153 * the [OOUI documentation on MediaWiki] [1] for more information.
7155 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7158 * @extends OO.ui.DecoratedOptionWidget
7161 * @param {Object} [config] Configuration options
7163 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7164 // Parent constructor
7165 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7168 this.checkIcon
= new OO
.ui
.IconWidget( {
7170 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7175 .prepend( this.checkIcon
.$element
)
7176 .addClass( 'oo-ui-menuOptionWidget' );
7181 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7183 /* Static Properties */
7189 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7192 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
7193 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
7196 * var myDropdown = new OO.ui.DropdownWidget( {
7199 * new OO.ui.MenuSectionOptionWidget( {
7202 * new OO.ui.MenuOptionWidget( {
7204 * label: 'Welsh Corgi'
7206 * new OO.ui.MenuOptionWidget( {
7208 * label: 'Standard Poodle'
7210 * new OO.ui.MenuSectionOptionWidget( {
7213 * new OO.ui.MenuOptionWidget( {
7220 * $( 'body' ).append( myDropdown.$element );
7223 * @extends OO.ui.DecoratedOptionWidget
7226 * @param {Object} [config] Configuration options
7228 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7229 // Parent constructor
7230 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7233 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
7234 .removeAttr( 'role aria-selected' );
7239 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7241 /* Static Properties */
7247 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7253 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7256 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7257 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7258 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
7259 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7260 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7261 * and customized to be opened, closed, and displayed as needed.
7263 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7264 * mouse outside the menu.
7266 * Menus also have support for keyboard interaction:
7268 * - Enter/Return key: choose and select a menu option
7269 * - Up-arrow key: highlight the previous menu option
7270 * - Down-arrow key: highlight the next menu option
7271 * - Esc key: hide the menu
7273 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7275 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7276 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7279 * @extends OO.ui.SelectWidget
7280 * @mixins OO.ui.mixin.ClippableElement
7281 * @mixins OO.ui.mixin.FloatableElement
7284 * @param {Object} [config] Configuration options
7285 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
7286 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
7287 * and {@link OO.ui.mixin.LookupElement LookupElement}
7288 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7289 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiselectWidget CapsuleMultiselectWidget}
7290 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
7291 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
7292 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
7293 * that button, unless the button (or its parent widget) is passed in here.
7294 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7295 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7296 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7297 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7298 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7299 * @cfg {number} [width] Width of the menu
7301 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7302 // Configuration initialization
7303 config
= config
|| {};
7305 // Parent constructor
7306 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7308 // Mixin constructors
7309 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7310 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7312 // Initial vertical positions other than 'center' will result in
7313 // the menu being flipped if there is not enough space in the container.
7314 // Store the original position so we know what to reset to.
7315 this.originalVerticalPosition
= this.verticalPosition
;
7318 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7319 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7320 this.filterFromInput
= !!config
.filterFromInput
;
7321 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7322 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7323 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7324 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7325 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7326 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7327 this.width
= config
.width
;
7330 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7331 if ( config
.widget
) {
7332 this.setFocusOwner( config
.widget
.$tabIndexed
);
7335 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7336 // that reference properties not initialized at that time of parent class construction
7337 // TODO: Find a better way to handle post-constructor setup
7338 this.visible
= false;
7339 this.$element
.addClass( 'oo-ui-element-hidden' );
7344 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7345 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7346 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7353 * The menu is ready: it is visible and has been positioned and clipped.
7356 /* Static properties */
7359 * Positions to flip to if there isn't room in the container for the
7360 * menu in a specific direction.
7362 * @property {Object.<string,string>}
7364 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7374 * Handles document mouse down events.
7377 * @param {MouseEvent} e Mouse down event
7379 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7383 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7388 this.toggle( false );
7395 OO
.ui
.MenuSelectWidget
.prototype.onKeyDown = function ( e
) {
7396 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7398 if ( !this.isDisabled() && this.isVisible() ) {
7399 switch ( e
.keyCode
) {
7400 case OO
.ui
.Keys
.LEFT
:
7401 case OO
.ui
.Keys
.RIGHT
:
7402 // Do nothing if a text field is associated, arrow keys will be handled natively
7403 if ( !this.$input
) {
7404 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7407 case OO
.ui
.Keys
.ESCAPE
:
7408 case OO
.ui
.Keys
.TAB
:
7409 if ( currentItem
) {
7410 currentItem
.setHighlighted( false );
7412 this.toggle( false );
7413 // Don't prevent tabbing away, prevent defocusing
7414 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7416 e
.stopPropagation();
7420 OO
.ui
.MenuSelectWidget
.parent
.prototype.onKeyDown
.call( this, e
);
7427 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7428 * or after items were added/removed (always).
7432 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7433 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7435 len
= this.items
.length
,
7436 showAll
= !this.isVisible(),
7439 if ( this.$input
&& this.filterFromInput
) {
7440 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
7441 exactFilter
= this.getItemMatcher( this.$input
.val(), true );
7442 // Hide non-matching options, and also hide section headers if all options
7443 // in their section are hidden.
7444 for ( i
= 0; i
< len
; i
++ ) {
7445 item
= this.items
[ i
];
7446 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7448 // If the previous section was empty, hide its header
7449 section
.toggle( showAll
|| !sectionEmpty
);
7452 sectionEmpty
= true;
7453 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7454 visible
= showAll
|| filter( item
);
7455 exactMatch
= exactMatch
|| exactFilter( item
);
7456 anyVisible
= anyVisible
|| visible
;
7457 sectionEmpty
= sectionEmpty
&& !visible
;
7458 item
.toggle( visible
);
7461 // Process the final section
7463 section
.toggle( showAll
|| !sectionEmpty
);
7466 if ( anyVisible
&& this.items
.length
&& !exactMatch
) {
7467 this.scrollItemIntoView( this.items
[ 0 ] );
7470 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7472 if ( this.highlightOnFilter
) {
7473 // Highlight the first item on the list
7475 items
= this.getItems();
7476 for ( i
= 0; i
< items
.length
; i
++ ) {
7477 if ( items
[ i
].isVisible() ) {
7482 this.highlightItem( item
);
7487 // Reevaluate clipping
7494 OO
.ui
.MenuSelectWidget
.prototype.bindKeyDownListener = function () {
7495 if ( this.$input
) {
7496 this.$input
.on( 'keydown', this.onKeyDownHandler
);
7498 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyDownListener
.call( this );
7505 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyDownListener = function () {
7506 if ( this.$input
) {
7507 this.$input
.off( 'keydown', this.onKeyDownHandler
);
7509 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyDownListener
.call( this );
7516 OO
.ui
.MenuSelectWidget
.prototype.bindKeyPressListener = function () {
7517 if ( this.$input
) {
7518 if ( this.filterFromInput
) {
7519 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7520 this.updateItemVisibility();
7523 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindKeyPressListener
.call( this );
7530 OO
.ui
.MenuSelectWidget
.prototype.unbindKeyPressListener = function () {
7531 if ( this.$input
) {
7532 if ( this.filterFromInput
) {
7533 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7534 this.updateItemVisibility();
7537 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindKeyPressListener
.call( this );
7544 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
7546 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
7547 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
7549 * @param {OO.ui.OptionWidget} item Item to choose
7552 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7553 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7554 if ( this.hideOnChoose
) {
7555 this.toggle( false );
7563 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7565 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7567 this.updateItemVisibility();
7575 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7577 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7579 this.updateItemVisibility();
7587 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7589 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7591 this.updateItemVisibility();
7597 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7598 * `.toggle( true )` after its #$element is attached to the DOM.
7600 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7601 * it in the right place and with the right dimensions only work correctly while it is attached.
7602 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7603 * strictly enforced, so currently it only generates a warning in the browser console.
7608 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7609 var change
, originalHeight
, flippedHeight
;
7611 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7612 change
= visible
!== this.isVisible();
7614 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7615 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7616 this.warnedUnattached
= true;
7619 if ( change
&& visible
) {
7620 // Reset position before showing the popup again. It's possible we no longer need to flip
7621 // (e.g. if the user scrolled).
7622 this.setVerticalPosition( this.originalVerticalPosition
);
7626 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7632 this.setIdealSize( this.width
);
7633 } else if ( this.$floatableContainer
) {
7634 this.$clippable
.css( 'width', 'auto' );
7636 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
7637 // Dropdown is smaller than handle so expand to width
7638 this.$floatableContainer
[ 0 ].offsetWidth
:
7639 // Dropdown is larger than handle so auto size
7642 this.$clippable
.css( 'width', '' );
7645 this.togglePositioning( !!this.$floatableContainer
);
7646 this.toggleClipping( true );
7648 this.bindKeyDownListener();
7649 this.bindKeyPressListener();
7652 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
7653 this.originalVerticalPosition
!== 'center'
7655 // If opening the menu in one direction causes it to be clipped, flip it
7656 originalHeight
= this.$element
.height();
7657 this.setVerticalPosition(
7658 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
7660 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
7661 // If flipping also causes it to be clipped, open in whichever direction
7662 // we have more space
7663 flippedHeight
= this.$element
.height();
7664 if ( originalHeight
> flippedHeight
) {
7665 this.setVerticalPosition( this.originalVerticalPosition
);
7669 // Note that we do not flip the menu's opening direction if the clipping changes
7670 // later (e.g. after the user scrolls), that seems like it would be annoying
7672 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7674 if ( this.findSelectedItem() ) {
7675 this.$focusOwner
.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() );
7676 this.findSelectedItem().scrollElementIntoView( { duration
: 0 } );
7680 if ( this.autoHide
) {
7681 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7684 this.emit( 'ready' );
7686 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7687 this.unbindKeyDownListener();
7688 this.unbindKeyPressListener();
7689 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7690 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7691 this.togglePositioning( false );
7692 this.toggleClipping( false );
7700 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7701 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7702 * users can interact with it.
7704 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7705 * OO.ui.DropdownInputWidget instead.
7708 * // Example: A DropdownWidget with a menu that contains three options
7709 * var dropDown = new OO.ui.DropdownWidget( {
7710 * label: 'Dropdown menu: Select a menu option',
7713 * new OO.ui.MenuOptionWidget( {
7717 * new OO.ui.MenuOptionWidget( {
7721 * new OO.ui.MenuOptionWidget( {
7729 * $( 'body' ).append( dropDown.$element );
7731 * dropDown.getMenu().selectItemByData( 'b' );
7733 * dropDown.getMenu().findSelectedItem().getData(); // returns 'b'
7735 * For more information, please see the [OOUI documentation on MediaWiki] [1].
7737 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7740 * @extends OO.ui.Widget
7741 * @mixins OO.ui.mixin.IconElement
7742 * @mixins OO.ui.mixin.IndicatorElement
7743 * @mixins OO.ui.mixin.LabelElement
7744 * @mixins OO.ui.mixin.TitledElement
7745 * @mixins OO.ui.mixin.TabIndexedElement
7748 * @param {Object} [config] Configuration options
7749 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.MenuSelectWidget menu select widget}
7750 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
7751 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
7752 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
7753 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
7755 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
7756 // Configuration initialization
7757 config
= $.extend( { indicator
: 'down' }, config
);
7759 // Parent constructor
7760 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
7762 // Properties (must be set before TabIndexedElement constructor call)
7763 this.$handle
= $( '<span>' );
7764 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
7766 // Mixin constructors
7767 OO
.ui
.mixin
.IconElement
.call( this, config
);
7768 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7769 OO
.ui
.mixin
.LabelElement
.call( this, config
);
7770 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
7771 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
7774 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
7776 $floatableContainer
: this.$element
7781 click
: this.onClick
.bind( this ),
7782 keydown
: this.onKeyDown
.bind( this ),
7783 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
7784 keypress
: this.menu
.onKeyPressHandler
,
7785 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
7787 this.menu
.connect( this, {
7788 select
: 'onMenuSelect',
7789 toggle
: 'onMenuToggle'
7794 .addClass( 'oo-ui-dropdownWidget-handle' )
7797 'aria-owns': this.menu
.getElementId(),
7798 'aria-autocomplete': 'list'
7800 .append( this.$icon
, this.$label
, this.$indicator
);
7802 .addClass( 'oo-ui-dropdownWidget' )
7803 .append( this.$handle
);
7804 this.$overlay
.append( this.menu
.$element
);
7809 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
7810 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
7811 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
7812 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
7813 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
7814 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
7821 * @return {OO.ui.MenuSelectWidget} Menu of widget
7823 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
7828 * Handles menu select events.
7831 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7833 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
7837 this.setLabel( null );
7841 selectedLabel
= item
.getLabel();
7843 // If the label is a DOM element, clone it, because setLabel will append() it
7844 if ( selectedLabel
instanceof jQuery
) {
7845 selectedLabel
= selectedLabel
.clone();
7848 this.setLabel( selectedLabel
);
7852 * Handle menu toggle events.
7855 * @param {boolean} isVisible Open state of the menu
7857 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
7858 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
7861 this.$element
.hasClass( 'oo-ui-dropdownWidget-open' ).toString()
7866 * Handle mouse click events.
7869 * @param {jQuery.Event} e Mouse click event
7871 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
7872 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
7879 * Handle key down events.
7882 * @param {jQuery.Event} e Key down event
7884 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
7886 !this.isDisabled() &&
7888 e
.which
=== OO
.ui
.Keys
.ENTER
||
7890 e
.which
=== OO
.ui
.Keys
.SPACE
&&
7891 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
7892 // Space only closes the menu is the user is not typing to search.
7893 this.menu
.keyPressBuffer
=== ''
7896 !this.menu
.isVisible() &&
7898 e
.which
=== OO
.ui
.Keys
.UP
||
7899 e
.which
=== OO
.ui
.Keys
.DOWN
7910 * RadioOptionWidget is an option widget that looks like a radio button.
7911 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
7912 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
7914 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
7917 * @extends OO.ui.OptionWidget
7920 * @param {Object} [config] Configuration options
7922 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
7923 // Configuration initialization
7924 config
= config
|| {};
7926 // Properties (must be done before parent constructor which calls #setDisabled)
7927 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
7929 // Parent constructor
7930 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
7933 // Remove implicit role, we're handling it ourselves
7934 this.radio
.$input
.attr( 'role', 'presentation' );
7936 .addClass( 'oo-ui-radioOptionWidget' )
7937 .attr( 'role', 'radio' )
7938 .attr( 'aria-checked', 'false' )
7939 .removeAttr( 'aria-selected' )
7940 .prepend( this.radio
.$element
);
7945 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
7947 /* Static Properties */
7953 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
7959 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
7965 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
7971 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
7978 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
7979 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
7981 this.radio
.setSelected( state
);
7983 .attr( 'aria-checked', state
.toString() )
7984 .removeAttr( 'aria-selected' );
7992 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
7993 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
7995 this.radio
.setDisabled( this.isDisabled() );
8001 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8002 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8003 * an interface for adding, removing and selecting options.
8004 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8006 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8007 * OO.ui.RadioSelectInputWidget instead.
8010 * // A RadioSelectWidget with RadioOptions.
8011 * var option1 = new OO.ui.RadioOptionWidget( {
8013 * label: 'Selected radio option'
8016 * var option2 = new OO.ui.RadioOptionWidget( {
8018 * label: 'Unselected radio option'
8021 * var radioSelect=new OO.ui.RadioSelectWidget( {
8022 * items: [ option1, option2 ]
8025 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8026 * radioSelect.selectItem( option1 );
8028 * $( 'body' ).append( radioSelect.$element );
8030 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8034 * @extends OO.ui.SelectWidget
8035 * @mixins OO.ui.mixin.TabIndexedElement
8038 * @param {Object} [config] Configuration options
8040 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8041 // Parent constructor
8042 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8044 // Mixin constructors
8045 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8049 focus
: this.bindKeyDownListener
.bind( this ),
8050 blur
: this.unbindKeyDownListener
.bind( this )
8055 .addClass( 'oo-ui-radioSelectWidget' )
8056 .attr( 'role', 'radiogroup' );
8061 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8062 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8065 * MultioptionWidgets are special elements that can be selected and configured with data. The
8066 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8067 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8068 * and examples, please see the [OOUI documentation on MediaWiki][1].
8070 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Multioptions
8073 * @extends OO.ui.Widget
8074 * @mixins OO.ui.mixin.ItemWidget
8075 * @mixins OO.ui.mixin.LabelElement
8078 * @param {Object} [config] Configuration options
8079 * @cfg {boolean} [selected=false] Whether the option is initially selected
8081 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8082 // Configuration initialization
8083 config
= config
|| {};
8085 // Parent constructor
8086 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8088 // Mixin constructors
8089 OO
.ui
.mixin
.ItemWidget
.call( this );
8090 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8093 this.selected
= null;
8097 .addClass( 'oo-ui-multioptionWidget' )
8098 .append( this.$label
);
8099 this.setSelected( config
.selected
);
8104 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8105 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8106 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8113 * A change event is emitted when the selected state of the option changes.
8115 * @param {boolean} selected Whether the option is now selected
8121 * Check if the option is selected.
8123 * @return {boolean} Item is selected
8125 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8126 return this.selected
;
8130 * Set the option’s selected state. In general, all modifications to the selection
8131 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
8132 * method instead of this method.
8134 * @param {boolean} [state=false] Select option
8137 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8139 if ( this.selected
!== state
) {
8140 this.selected
= state
;
8141 this.emit( 'change', state
);
8142 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8148 * MultiselectWidget allows selecting multiple options from a list.
8150 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
8152 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8156 * @extends OO.ui.Widget
8157 * @mixins OO.ui.mixin.GroupWidget
8160 * @param {Object} [config] Configuration options
8161 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8163 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8164 // Parent constructor
8165 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8167 // Configuration initialization
8168 config
= config
|| {};
8170 // Mixin constructors
8171 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8174 this.aggregate( { change
: 'select' } );
8175 // This is mostly for compatibility with CapsuleMultiselectWidget... normally, 'change' is emitted
8176 // by GroupElement only when items are added/removed
8177 this.connect( this, { select
: [ 'emit', 'change' ] } );
8180 if ( config
.items
) {
8181 this.addItems( config
.items
);
8183 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8184 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8185 .append( this.$group
);
8190 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8191 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8198 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8204 * A select event is emitted when an item is selected or deselected.
8210 * Find options that are selected.
8212 * @return {OO.ui.MultioptionWidget[]} Selected options
8214 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8215 return this.items
.filter( function ( item
) {
8216 return item
.isSelected();
8221 * Find the data of options that are selected.
8223 * @return {Object[]|string[]} Values of selected options
8225 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8226 return this.findSelectedItems().map( function ( item
) {
8232 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8234 * @param {OO.ui.MultioptionWidget[]} items Items to select
8237 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8238 this.items
.forEach( function ( item
) {
8239 var selected
= items
.indexOf( item
) !== -1;
8240 item
.setSelected( selected
);
8246 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8248 * @param {Object[]|string[]} datas Values of items to select
8251 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8254 items
= datas
.map( function ( data
) {
8255 return widget
.findItemFromData( data
);
8257 this.selectItems( items
);
8262 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8263 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8264 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8266 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8269 * @extends OO.ui.MultioptionWidget
8272 * @param {Object} [config] Configuration options
8274 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8275 // Configuration initialization
8276 config
= config
|| {};
8278 // Properties (must be done before parent constructor which calls #setDisabled)
8279 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8281 // Parent constructor
8282 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8285 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8286 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8290 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8291 .prepend( this.checkbox
.$element
);
8296 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8298 /* Static Properties */
8304 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8309 * Handle checkbox selected state change.
8313 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8314 this.setSelected( this.checkbox
.isSelected() );
8320 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8321 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8322 this.checkbox
.setSelected( state
);
8329 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8330 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8331 this.checkbox
.setDisabled( this.isDisabled() );
8338 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8339 this.checkbox
.focus();
8343 * Handle key down events.
8346 * @param {jQuery.Event} e
8348 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8350 element
= this.getElementGroup(),
8353 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8354 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8355 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8356 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8366 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8367 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8368 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8369 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8371 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8372 * OO.ui.CheckboxMultiselectInputWidget instead.
8375 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8376 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8379 * label: 'Selected checkbox'
8382 * var option2 = new OO.ui.CheckboxMultioptionWidget( {
8384 * label: 'Unselected checkbox'
8387 * var multiselect=new OO.ui.CheckboxMultiselectWidget( {
8388 * items: [ option1, option2 ]
8391 * $( 'body' ).append( multiselect.$element );
8393 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8396 * @extends OO.ui.MultiselectWidget
8399 * @param {Object} [config] Configuration options
8401 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8402 // Parent constructor
8403 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8406 this.$lastClicked
= null;
8409 this.$group
.on( 'click', this.onClick
.bind( this ) );
8413 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8418 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8423 * Get an option by its position relative to the specified item (or to the start of the option array,
8424 * if item is `null`). The direction in which to search through the option array is specified with a
8425 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8426 * `null` if there are no options in the array.
8428 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8429 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8430 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8432 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8433 var currentIndex
, nextIndex
, i
,
8434 increase
= direction
> 0 ? 1 : -1,
8435 len
= this.items
.length
;
8438 currentIndex
= this.items
.indexOf( item
);
8439 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8441 // If no item is selected and moving forward, start at the beginning.
8442 // If moving backward, start at the end.
8443 nextIndex
= direction
> 0 ? 0 : len
- 1;
8446 for ( i
= 0; i
< len
; i
++ ) {
8447 item
= this.items
[ nextIndex
];
8448 if ( item
&& !item
.isDisabled() ) {
8451 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8457 * Handle click events on checkboxes.
8459 * @param {jQuery.Event} e
8461 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8462 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8463 $lastClicked
= this.$lastClicked
,
8464 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8465 .not( '.oo-ui-widget-disabled' );
8467 // Allow selecting multiple options at once by Shift-clicking them
8468 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8469 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8470 lastClickedIndex
= $options
.index( $lastClicked
);
8471 nowClickedIndex
= $options
.index( $nowClicked
);
8472 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8473 // browser. In either case we don't need custom handling.
8474 if ( nowClickedIndex
!== lastClickedIndex
) {
8476 wasSelected
= items
[ nowClickedIndex
].isSelected();
8477 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8479 // This depends on the DOM order of the items and the order of the .items array being the same.
8480 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8481 if ( !items
[ i
].isDisabled() ) {
8482 items
[ i
].setSelected( !wasSelected
);
8485 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8486 // handling first, then set our value. The order in which events happen is different for
8487 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8488 // non-click actions that change the checkboxes.
8490 setTimeout( function () {
8491 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8492 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8498 if ( $nowClicked
.length
) {
8499 this.$lastClicked
= $nowClicked
;
8508 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8510 if ( !this.isDisabled() ) {
8511 item
= this.getRelativeFocusableItem( null, 1 );
8522 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8527 * Progress bars visually display the status of an operation, such as a download,
8528 * and can be either determinate or indeterminate:
8530 * - **determinate** process bars show the percent of an operation that is complete.
8532 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8533 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8534 * not use percentages.
8536 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8539 * // Examples of determinate and indeterminate progress bars.
8540 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8543 * var progressBar2 = new OO.ui.ProgressBarWidget();
8545 * // Create a FieldsetLayout to layout progress bars
8546 * var fieldset = new OO.ui.FieldsetLayout;
8547 * fieldset.addItems( [
8548 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
8549 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
8551 * $( 'body' ).append( fieldset.$element );
8554 * @extends OO.ui.Widget
8557 * @param {Object} [config] Configuration options
8558 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8559 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8560 * By default, the progress bar is indeterminate.
8562 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8563 // Configuration initialization
8564 config
= config
|| {};
8566 // Parent constructor
8567 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8570 this.$bar
= $( '<div>' );
8571 this.progress
= null;
8574 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8575 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8578 role
: 'progressbar',
8580 'aria-valuemax': 100
8582 .addClass( 'oo-ui-progressBarWidget' )
8583 .append( this.$bar
);
8588 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8590 /* Static Properties */
8596 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8601 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8603 * @return {number|boolean} Progress percent
8605 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8606 return this.progress
;
8610 * Set the percent of the process completed or `false` for an indeterminate process.
8612 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8614 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8615 this.progress
= progress
;
8617 if ( progress
!== false ) {
8618 this.$bar
.css( 'width', this.progress
+ '%' );
8619 this.$element
.attr( 'aria-valuenow', this.progress
);
8621 this.$bar
.css( 'width', '' );
8622 this.$element
.removeAttr( 'aria-valuenow' );
8624 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8628 * InputWidget is the base class for all input widgets, which
8629 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8630 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8631 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
8633 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
8637 * @extends OO.ui.Widget
8638 * @mixins OO.ui.mixin.FlaggedElement
8639 * @mixins OO.ui.mixin.TabIndexedElement
8640 * @mixins OO.ui.mixin.TitledElement
8641 * @mixins OO.ui.mixin.AccessKeyedElement
8644 * @param {Object} [config] Configuration options
8645 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8646 * @cfg {string} [value=''] The value of the input.
8647 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8648 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8649 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8650 * before it is accepted.
8652 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8653 // Configuration initialization
8654 config
= config
|| {};
8656 // Parent constructor
8657 OO
.ui
.InputWidget
.parent
.call( this, config
);
8660 // See #reusePreInfuseDOM about config.$input
8661 this.$input
= config
.$input
|| this.getInputElement( config
);
8663 this.inputFilter
= config
.inputFilter
;
8665 // Mixin constructors
8666 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8667 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8668 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8669 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8672 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8676 .addClass( 'oo-ui-inputWidget-input' )
8677 .attr( 'name', config
.name
)
8678 .prop( 'disabled', this.isDisabled() );
8680 .addClass( 'oo-ui-inputWidget' )
8681 .append( this.$input
);
8682 this.setValue( config
.value
);
8684 this.setDir( config
.dir
);
8686 if ( config
.inputId
!== undefined ) {
8687 this.setInputId( config
.inputId
);
8693 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8694 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8695 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8696 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8697 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8699 /* Static Methods */
8704 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8705 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8706 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8707 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8714 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8715 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8716 if ( config
.$input
&& config
.$input
.length
) {
8717 state
.value
= config
.$input
.val();
8718 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8719 state
.focus
= config
.$input
.is( ':focus' );
8729 * A change event is emitted when the value of the input changes.
8731 * @param {string} value
8737 * Get input element.
8739 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
8740 * different circumstances. The element must have a `value` property (like form elements).
8743 * @param {Object} config Configuration options
8744 * @return {jQuery} Input element
8746 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
8747 return $( '<input>' );
8751 * Handle potentially value-changing events.
8754 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8756 OO
.ui
.InputWidget
.prototype.onEdit = function () {
8758 if ( !this.isDisabled() ) {
8759 // Allow the stack to clear so the value will be updated
8760 setTimeout( function () {
8761 widget
.setValue( widget
.$input
.val() );
8767 * Get the value of the input.
8769 * @return {string} Input value
8771 OO
.ui
.InputWidget
.prototype.getValue = function () {
8772 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
8773 // it, and we won't know unless they're kind enough to trigger a 'change' event.
8774 var value
= this.$input
.val();
8775 if ( this.value
!== value
) {
8776 this.setValue( value
);
8782 * Set the directionality of the input.
8784 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
8787 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
8788 this.$input
.prop( 'dir', dir
);
8793 * Set the value of the input.
8795 * @param {string} value New value
8799 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
8800 value
= this.cleanUpValue( value
);
8801 // Update the DOM if it has changed. Note that with cleanUpValue, it
8802 // is possible for the DOM value to change without this.value changing.
8803 if ( this.$input
.val() !== value
) {
8804 this.$input
.val( value
);
8806 if ( this.value
!== value
) {
8808 this.emit( 'change', this.value
);
8810 // The first time that the value is set (probably while constructing the widget),
8811 // remember it in defaultValue. This property can be later used to check whether
8812 // the value of the input has been changed since it was created.
8813 if ( this.defaultValue
=== undefined ) {
8814 this.defaultValue
= this.value
;
8815 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
8821 * Clean up incoming value.
8823 * Ensures value is a string, and converts undefined and null to empty string.
8826 * @param {string} value Original value
8827 * @return {string} Cleaned up value
8829 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
8830 if ( value
=== undefined || value
=== null ) {
8832 } else if ( this.inputFilter
) {
8833 return this.inputFilter( String( value
) );
8835 return String( value
);
8842 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
8843 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
8844 if ( this.$input
) {
8845 this.$input
.prop( 'disabled', this.isDisabled() );
8851 * Set the 'id' attribute of the `<input>` element.
8853 * @param {string} id
8856 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
8857 this.$input
.attr( 'id', id
);
8864 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
8865 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
8866 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
8867 this.setValue( state
.value
);
8869 if ( state
.focus
) {
8875 * Data widget intended for creating 'hidden'-type inputs.
8878 * @extends OO.ui.Widget
8881 * @param {Object} [config] Configuration options
8882 * @cfg {string} [value=''] The value of the input.
8883 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8885 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
8886 // Configuration initialization
8887 config
= $.extend( { value
: '', name
: '' }, config
);
8889 // Parent constructor
8890 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
8893 this.$element
.attr( {
8895 value
: config
.value
,
8898 this.$element
.removeAttr( 'aria-disabled' );
8903 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
8905 /* Static Properties */
8911 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
8914 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
8915 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
8916 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
8917 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
8918 * [OOUI documentation on MediaWiki] [1] for more information.
8921 * // A ButtonInputWidget rendered as an HTML button, the default.
8922 * var button = new OO.ui.ButtonInputWidget( {
8923 * label: 'Input button',
8927 * $( 'body' ).append( button.$element );
8929 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
8932 * @extends OO.ui.InputWidget
8933 * @mixins OO.ui.mixin.ButtonElement
8934 * @mixins OO.ui.mixin.IconElement
8935 * @mixins OO.ui.mixin.IndicatorElement
8936 * @mixins OO.ui.mixin.LabelElement
8937 * @mixins OO.ui.mixin.TitledElement
8940 * @param {Object} [config] Configuration options
8941 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
8942 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
8943 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
8944 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
8945 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
8947 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
8948 // Configuration initialization
8949 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
8951 // See InputWidget#reusePreInfuseDOM about config.$input
8952 if ( config
.$input
) {
8953 config
.$input
.empty();
8956 // Properties (must be set before parent constructor, which calls #setValue)
8957 this.useInputTag
= config
.useInputTag
;
8959 // Parent constructor
8960 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
8962 // Mixin constructors
8963 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
8964 OO
.ui
.mixin
.IconElement
.call( this, config
);
8965 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8966 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8967 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8970 if ( !config
.useInputTag
) {
8971 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
8973 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
8978 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
8979 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
8980 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
8981 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
8982 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
8983 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.TitledElement
);
8985 /* Static Properties */
8991 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
8999 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9001 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9002 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9008 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9010 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9011 * text, or `null` for no label
9014 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9015 if ( typeof label
=== 'function' ) {
9016 label
= OO
.ui
.resolveMsg( label
);
9019 if ( this.useInputTag
) {
9020 // Discard non-plaintext labels
9021 if ( typeof label
!== 'string' ) {
9025 this.$input
.val( label
);
9028 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9032 * Set the value of the input.
9034 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9035 * they do not support {@link #value values}.
9037 * @param {string} value New value
9040 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9041 if ( !this.useInputTag
) {
9042 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9050 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9051 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
9052 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
9057 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9058 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9059 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9060 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9062 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9065 * // An example of selected, unselected, and disabled checkbox inputs
9066 * var checkbox1=new OO.ui.CheckboxInputWidget( {
9070 * var checkbox2=new OO.ui.CheckboxInputWidget( {
9073 * var checkbox3=new OO.ui.CheckboxInputWidget( {
9077 * // Create a fieldset layout with fields for each checkbox.
9078 * var fieldset = new OO.ui.FieldsetLayout( {
9079 * label: 'Checkboxes'
9081 * fieldset.addItems( [
9082 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9083 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9084 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9086 * $( 'body' ).append( fieldset.$element );
9088 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9091 * @extends OO.ui.InputWidget
9094 * @param {Object} [config] Configuration options
9095 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
9097 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9098 // Configuration initialization
9099 config
= config
|| {};
9101 // Parent constructor
9102 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9105 this.checkIcon
= new OO
.ui
.IconWidget( {
9107 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9112 .addClass( 'oo-ui-checkboxInputWidget' )
9113 // Required for pretty styling in WikimediaUI theme
9114 .append( this.checkIcon
.$element
);
9115 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9120 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9122 /* Static Properties */
9128 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9130 /* Static Methods */
9135 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9136 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9137 state
.checked
= config
.$input
.prop( 'checked' );
9147 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9148 return $( '<input>' ).attr( 'type', 'checkbox' );
9154 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9156 if ( !this.isDisabled() ) {
9157 // Allow the stack to clear so the value will be updated
9158 setTimeout( function () {
9159 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9165 * Set selection state of this checkbox.
9167 * @param {boolean} state `true` for selected
9170 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9172 if ( this.selected
!== state
) {
9173 this.selected
= state
;
9174 this.$input
.prop( 'checked', this.selected
);
9175 this.emit( 'change', this.selected
);
9177 // The first time that the selection state is set (probably while constructing the widget),
9178 // remember it in defaultSelected. This property can be later used to check whether
9179 // the selection state of the input has been changed since it was created.
9180 if ( this.defaultSelected
=== undefined ) {
9181 this.defaultSelected
= this.selected
;
9182 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9188 * Check if this checkbox is selected.
9190 * @return {boolean} Checkbox is selected
9192 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9193 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9194 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9195 var selected
= this.$input
.prop( 'checked' );
9196 if ( this.selected
!== selected
) {
9197 this.setSelected( selected
);
9199 return this.selected
;
9205 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9206 if ( !this.isDisabled() ) {
9207 this.$input
.click();
9215 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9216 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9217 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9218 this.setSelected( state
.checked
);
9223 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9224 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9225 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9226 * more information about input widgets.
9228 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9229 * are no options. If no `value` configuration option is provided, the first option is selected.
9230 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9232 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
9235 * // Example: A DropdownInputWidget with three options
9236 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9238 * { data: 'a', label: 'First' },
9239 * { data: 'b', label: 'Second'},
9240 * { data: 'c', label: 'Third' }
9243 * $( 'body' ).append( dropdownInput.$element );
9245 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9248 * @extends OO.ui.InputWidget
9251 * @param {Object} [config] Configuration options
9252 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9253 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9255 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9256 // Configuration initialization
9257 config
= config
|| {};
9259 // Properties (must be done before parent constructor which calls #setDisabled)
9260 this.dropdownWidget
= new OO
.ui
.DropdownWidget( config
.dropdown
);
9261 // Set up the options before parent constructor, which uses them to validate config.value.
9262 // Use this instead of setOptions() because this.$input is not set up yet.
9263 this.setOptionsData( config
.options
|| [] );
9265 // Parent constructor
9266 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9269 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
9273 .addClass( 'oo-ui-dropdownInputWidget' )
9274 .append( this.dropdownWidget
.$element
);
9275 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9280 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9288 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9289 return $( '<select>' );
9293 * Handles menu select events.
9296 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9298 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9299 this.setValue( item
? item
.getData() : '' );
9305 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9307 value
= this.cleanUpValue( value
);
9308 // Only allow setting values that are actually present in the dropdown
9309 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9310 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9311 this.dropdownWidget
.getMenu().selectItem( selected
);
9312 value
= selected
? selected
.getData() : '';
9313 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9314 if ( this.optionsDirty
) {
9315 // We reached this from the constructor or from #setOptions.
9316 // We have to update the <select> element.
9317 this.updateOptionsInterface();
9325 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9326 this.dropdownWidget
.setDisabled( state
);
9327 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9332 * Set the options available for this input.
9334 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9337 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9338 var value
= this.getValue();
9340 this.setOptionsData( options
);
9342 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9343 // In case the previous value is no longer an available option, select the first valid one.
9344 this.setValue( value
);
9350 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9352 * This method may be called before the parent constructor, so various properties may not be
9355 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9358 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9363 this.optionsDirty
= true;
9365 optionWidgets
= options
.map( function ( opt
) {
9368 if ( opt
.optgroup
!== undefined ) {
9369 return widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9372 optValue
= widget
.cleanUpValue( opt
.data
);
9373 return widget
.createMenuOptionWidget(
9375 opt
.label
!== undefined ? opt
.label
: optValue
9380 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9384 * Create a menu option widget.
9387 * @param {string} data Item data
9388 * @param {string} label Item label
9389 * @return {OO.ui.MenuOptionWidget} Option widget
9391 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9392 return new OO
.ui
.MenuOptionWidget( {
9399 * Create a menu section option widget.
9402 * @param {string} label Section item label
9403 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9405 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9406 return new OO
.ui
.MenuSectionOptionWidget( {
9412 * Update the user-visible interface to match the internal list of options and value.
9414 * This method must only be called after the parent constructor.
9418 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9420 $optionsContainer
= this.$input
,
9421 defaultValue
= this.defaultValue
,
9424 this.$input
.empty();
9426 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9429 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9430 $optionNode
= $( '<option>' )
9431 .attr( 'value', optionWidget
.getData() )
9432 .text( optionWidget
.getLabel() );
9434 // Remember original selection state. This property can be later used to check whether
9435 // the selection state of the input has been changed since it was created.
9436 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9438 $optionsContainer
.append( $optionNode
);
9440 $optionNode
= $( '<optgroup>' )
9441 .attr( 'label', optionWidget
.getLabel() );
9442 widget
.$input
.append( $optionNode
);
9443 $optionsContainer
= $optionNode
;
9447 this.optionsDirty
= false;
9453 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9454 this.dropdownWidget
.focus();
9461 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9462 this.dropdownWidget
.blur();
9467 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9468 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9469 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9470 * please see the [OOUI documentation on MediaWiki][1].
9472 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9475 * // An example of selected, unselected, and disabled radio inputs
9476 * var radio1 = new OO.ui.RadioInputWidget( {
9480 * var radio2 = new OO.ui.RadioInputWidget( {
9483 * var radio3 = new OO.ui.RadioInputWidget( {
9487 * // Create a fieldset layout with fields for each radio button.
9488 * var fieldset = new OO.ui.FieldsetLayout( {
9489 * label: 'Radio inputs'
9491 * fieldset.addItems( [
9492 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9493 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9494 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9496 * $( 'body' ).append( fieldset.$element );
9498 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9501 * @extends OO.ui.InputWidget
9504 * @param {Object} [config] Configuration options
9505 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9507 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9508 // Configuration initialization
9509 config
= config
|| {};
9511 // Parent constructor
9512 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9516 .addClass( 'oo-ui-radioInputWidget' )
9517 // Required for pretty styling in WikimediaUI theme
9518 .append( $( '<span>' ) );
9519 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9524 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9526 /* Static Properties */
9532 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9534 /* Static Methods */
9539 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9540 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9541 state
.checked
= config
.$input
.prop( 'checked' );
9551 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9552 return $( '<input>' ).attr( 'type', 'radio' );
9558 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9559 // RadioInputWidget doesn't track its state.
9563 * Set selection state of this radio button.
9565 * @param {boolean} state `true` for selected
9568 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9569 // RadioInputWidget doesn't track its state.
9570 this.$input
.prop( 'checked', state
);
9571 // The first time that the selection state is set (probably while constructing the widget),
9572 // remember it in defaultSelected. This property can be later used to check whether
9573 // the selection state of the input has been changed since it was created.
9574 if ( this.defaultSelected
=== undefined ) {
9575 this.defaultSelected
= state
;
9576 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9582 * Check if this radio button is selected.
9584 * @return {boolean} Radio is selected
9586 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9587 return this.$input
.prop( 'checked' );
9593 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9594 if ( !this.isDisabled() ) {
9595 this.$input
.click();
9603 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9604 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9605 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9606 this.setSelected( state
.checked
);
9611 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9612 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9613 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9614 * more information about input widgets.
9616 * This and OO.ui.DropdownInputWidget support the same configuration options.
9619 * // Example: A RadioSelectInputWidget with three options
9620 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9622 * { data: 'a', label: 'First' },
9623 * { data: 'b', label: 'Second'},
9624 * { data: 'c', label: 'Third' }
9627 * $( 'body' ).append( radioSelectInput.$element );
9629 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9632 * @extends OO.ui.InputWidget
9635 * @param {Object} [config] Configuration options
9636 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9638 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9639 // Configuration initialization
9640 config
= config
|| {};
9642 // Properties (must be done before parent constructor which calls #setDisabled)
9643 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9644 // Set up the options before parent constructor, which uses them to validate config.value.
9645 // Use this instead of setOptions() because this.$input is not set up yet
9646 this.setOptionsData( config
.options
|| [] );
9648 // Parent constructor
9649 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9652 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9656 .addClass( 'oo-ui-radioSelectInputWidget' )
9657 .append( this.radioSelectWidget
.$element
);
9658 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
9663 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9665 /* Static Methods */
9670 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9671 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9672 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9679 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9680 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9681 // Cannot reuse the `<input type=radio>` set
9682 delete config
.$input
;
9692 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
9693 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
9694 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
9695 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
9699 * Handles menu select events.
9702 * @param {OO.ui.RadioOptionWidget} item Selected menu item
9704 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
9705 this.setValue( item
.getData() );
9711 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
9713 value
= this.cleanUpValue( value
);
9714 // Only allow setting values that are actually present in the dropdown
9715 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
9716 this.radioSelectWidget
.findFirstSelectableItem();
9717 this.radioSelectWidget
.selectItem( selected
);
9718 value
= selected
? selected
.getData() : '';
9719 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9726 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
9727 this.radioSelectWidget
.setDisabled( state
);
9728 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9733 * Set the options available for this input.
9735 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9738 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
9739 var value
= this.getValue();
9741 this.setOptionsData( options
);
9743 // Re-set the value to update the visible interface (RadioSelectWidget).
9744 // In case the previous value is no longer an available option, select the first valid one.
9745 this.setValue( value
);
9751 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9753 * This method may be called before the parent constructor, so various properties may not be
9756 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9759 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
9762 this.radioSelectWidget
9764 .addItems( options
.map( function ( opt
) {
9765 var optValue
= widget
.cleanUpValue( opt
.data
);
9766 return new OO
.ui
.RadioOptionWidget( {
9768 label
: opt
.label
!== undefined ? opt
.label
: optValue
9776 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
9777 this.radioSelectWidget
.focus();
9784 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
9785 this.radioSelectWidget
.blur();
9790 * CheckboxMultiselectInputWidget is a
9791 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
9792 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
9793 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
9794 * more information about input widgets.
9797 * // Example: A CheckboxMultiselectInputWidget with three options
9798 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
9800 * { data: 'a', label: 'First' },
9801 * { data: 'b', label: 'Second'},
9802 * { data: 'c', label: 'Third' }
9805 * $( 'body' ).append( multiselectInput.$element );
9807 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9810 * @extends OO.ui.InputWidget
9813 * @param {Object} [config] Configuration options
9814 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
9816 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
9817 // Configuration initialization
9818 config
= config
|| {};
9820 // Properties (must be done before parent constructor which calls #setDisabled)
9821 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
9822 // Must be set before the #setOptionsData call below
9823 this.inputName
= config
.name
;
9824 // Set up the options before parent constructor, which uses them to validate config.value.
9825 // Use this instead of setOptions() because this.$input is not set up yet
9826 this.setOptionsData( config
.options
|| [] );
9828 // Parent constructor
9829 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
9832 this.checkboxMultiselectWidget
.connect( this, { select
: 'onCheckboxesSelect' } );
9836 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
9837 .append( this.checkboxMultiselectWidget
.$element
);
9838 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
9839 this.$input
.detach();
9844 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
9846 /* Static Methods */
9851 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9852 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9853 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9854 .toArray().map( function ( el
) { return el
.value
; } );
9861 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9862 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9863 // Cannot reuse the `<input type=checkbox>` set
9864 delete config
.$input
;
9874 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
9876 return $( '<unused>' );
9880 * Handles CheckboxMultiselectWidget select events.
9884 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
9885 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
9891 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
9892 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
9893 .toArray().map( function ( el
) { return el
.value
; } );
9894 if ( this.value
!== value
) {
9895 this.setValue( value
);
9903 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
9904 value
= this.cleanUpValue( value
);
9905 this.checkboxMultiselectWidget
.selectItemsByData( value
);
9906 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
9907 if ( this.optionsDirty
) {
9908 // We reached this from the constructor or from #setOptions.
9909 // We have to update the <select> element.
9910 this.updateOptionsInterface();
9916 * Clean up incoming value.
9918 * @param {string[]} value Original value
9919 * @return {string[]} Cleaned up value
9921 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
9924 if ( !Array
.isArray( value
) ) {
9927 for ( i
= 0; i
< value
.length
; i
++ ) {
9929 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
9930 // Remove options that we don't have here
9931 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
9934 cleanValue
.push( singleValue
);
9942 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
9943 this.checkboxMultiselectWidget
.setDisabled( state
);
9944 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9949 * Set the options available for this input.
9951 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
9954 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
9955 var value
= this.getValue();
9957 this.setOptionsData( options
);
9959 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
9960 // This will also get rid of any stale options that we just removed.
9961 this.setValue( value
);
9967 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9969 * This method may be called before the parent constructor, so various properties may not be
9972 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9975 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
9978 this.optionsDirty
= true;
9980 this.checkboxMultiselectWidget
9982 .addItems( options
.map( function ( opt
) {
9983 var optValue
, item
, optDisabled
;
9985 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
9986 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
9987 item
= new OO
.ui
.CheckboxMultioptionWidget( {
9989 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
9990 disabled
: optDisabled
9992 // Set the 'name' and 'value' for form submission
9993 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
9994 item
.checkbox
.setValue( optValue
);
10000 * Update the user-visible interface to match the internal list of options and value.
10002 * This method must only be called after the parent constructor.
10006 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10007 var defaultValue
= this.defaultValue
;
10009 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10010 // Remember original selection state. This property can be later used to check whether
10011 // the selection state of the input has been changed since it was created.
10012 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10013 item
.checkbox
.defaultSelected
= isDefault
;
10014 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10017 this.optionsDirty
= false;
10023 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10024 this.checkboxMultiselectWidget
.focus();
10029 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10030 * size of the field as well as its presentation. In addition, these widgets can be configured
10031 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
10032 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
10033 * which modifies incoming values rather than validating them.
10034 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10036 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10039 * // Example of a text input widget
10040 * var textInput = new OO.ui.TextInputWidget( {
10041 * value: 'Text input'
10043 * $( 'body' ).append( textInput.$element );
10045 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10048 * @extends OO.ui.InputWidget
10049 * @mixins OO.ui.mixin.IconElement
10050 * @mixins OO.ui.mixin.IndicatorElement
10051 * @mixins OO.ui.mixin.PendingElement
10052 * @mixins OO.ui.mixin.LabelElement
10055 * @param {Object} [config] Configuration options
10056 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10057 * 'email', 'url' or 'number'.
10058 * @cfg {string} [placeholder] Placeholder text
10059 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10060 * instruct the browser to focus this widget.
10061 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10062 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10064 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10065 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10066 * many emojis) count as 2 characters each.
10067 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10068 * the value or placeholder text: `'before'` or `'after'`
10069 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator: 'required'`.
10070 * Note that `false` & setting `indicator: 'required' will result in no indicator shown.
10071 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10072 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined` means
10073 * leaving it up to the browser).
10074 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10075 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10076 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10077 * value for it to be considered valid; when Function, a function receiving the value as parameter
10078 * that must return true, or promise resolving to true, for it to be considered valid.
10080 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10081 // Configuration initialization
10082 config
= $.extend( {
10084 labelPosition
: 'after'
10087 if ( config
.multiline
) {
10088 OO
.ui
.warnDeprecation( 'TextInputWidget: config.multiline is deprecated. Use the MultilineTextInputWidget instead. See T130434.' );
10089 return new OO
.ui
.MultilineTextInputWidget( config
);
10092 // Parent constructor
10093 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10095 // Mixin constructors
10096 OO
.ui
.mixin
.IconElement
.call( this, config
);
10097 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10098 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
10099 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10102 this.type
= this.getSaneType( config
);
10103 this.readOnly
= false;
10104 this.required
= false;
10105 this.validate
= null;
10106 this.styleHeight
= null;
10107 this.scrollWidth
= null;
10109 this.setValidation( config
.validate
);
10110 this.setLabelPosition( config
.labelPosition
);
10114 keypress
: this.onKeyPress
.bind( this ),
10115 blur
: this.onBlur
.bind( this ),
10116 focus
: this.onFocus
.bind( this )
10118 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10119 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10120 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10121 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10125 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10126 .append( this.$icon
, this.$indicator
);
10127 this.setReadOnly( !!config
.readOnly
);
10128 this.setRequired( !!config
.required
);
10129 if ( config
.placeholder
!== undefined ) {
10130 this.$input
.attr( 'placeholder', config
.placeholder
);
10132 if ( config
.maxLength
!== undefined ) {
10133 this.$input
.attr( 'maxlength', config
.maxLength
);
10135 if ( config
.autofocus
) {
10136 this.$input
.attr( 'autofocus', 'autofocus' );
10138 if ( config
.autocomplete
=== false ) {
10139 this.$input
.attr( 'autocomplete', 'off' );
10140 // Turning off autocompletion also disables "form caching" when the user navigates to a
10141 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
10143 beforeunload: function () {
10144 this.$input
.removeAttr( 'autocomplete' );
10146 pageshow: function () {
10147 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
10148 // whole page... it shouldn't hurt, though.
10149 this.$input
.attr( 'autocomplete', 'off' );
10153 if ( config
.spellcheck
!== undefined ) {
10154 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10156 if ( this.label
) {
10157 this.isWaitingToBeAttached
= true;
10158 this.installParentChangeDetector();
10164 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10165 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10166 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10167 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10168 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10170 /* Static Properties */
10172 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10180 * An `enter` event is emitted when the user presses 'enter' inside the text box.
10188 * Handle icon mouse down events.
10191 * @param {jQuery.Event} e Mouse down event
10193 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10194 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10201 * Handle indicator mouse down events.
10204 * @param {jQuery.Event} e Mouse down event
10206 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10207 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10214 * Handle key press events.
10217 * @param {jQuery.Event} e Key press event
10218 * @fires enter If enter key is pressed
10220 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10221 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10222 this.emit( 'enter', e
);
10227 * Handle blur events.
10230 * @param {jQuery.Event} e Blur event
10232 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10233 this.setValidityFlag();
10237 * Handle focus events.
10240 * @param {jQuery.Event} e Focus event
10242 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10243 if ( this.isWaitingToBeAttached
) {
10244 // If we've received focus, then we must be attached to the document, and if
10245 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10246 this.onElementAttach();
10248 this.setValidityFlag( true );
10252 * Handle element attach events.
10255 * @param {jQuery.Event} e Element attach event
10257 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10258 this.isWaitingToBeAttached
= false;
10259 // Any previously calculated size is now probably invalid if we reattached elsewhere
10260 this.valCache
= null;
10261 this.positionLabel();
10265 * Handle debounced change events.
10267 * @param {string} value
10270 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10271 this.setValidityFlag();
10275 * Check if the input is {@link #readOnly read-only}.
10277 * @return {boolean}
10279 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10280 return this.readOnly
;
10284 * Set the {@link #readOnly read-only} state of the input.
10286 * @param {boolean} state Make input read-only
10289 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10290 this.readOnly
= !!state
;
10291 this.$input
.prop( 'readOnly', this.readOnly
);
10296 * Check if the input is {@link #required required}.
10298 * @return {boolean}
10300 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10301 return this.required
;
10305 * Set the {@link #required required} state of the input.
10307 * @param {boolean} state Make input required
10310 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10311 this.required
= !!state
;
10312 if ( this.required
) {
10314 .prop( 'required', true )
10315 .attr( 'aria-required', 'true' );
10316 if ( this.getIndicator() === null ) {
10317 this.setIndicator( 'required' );
10321 .prop( 'required', false )
10322 .removeAttr( 'aria-required' );
10323 if ( this.getIndicator() === 'required' ) {
10324 this.setIndicator( null );
10331 * Support function for making #onElementAttach work across browsers.
10333 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10334 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10336 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10337 * first time that the element gets attached to the documented.
10339 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10340 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10341 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
10344 if ( MutationObserver
) {
10345 // The new way. If only it wasn't so ugly.
10347 if ( this.isElementAttached() ) {
10348 // Widget is attached already, do nothing. This breaks the functionality of this function when
10349 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
10350 // would require observation of the whole document, which would hurt performance of other,
10351 // more important code.
10355 // Find topmost node in the tree
10356 topmostNode
= this.$element
[ 0 ];
10357 while ( topmostNode
.parentNode
) {
10358 topmostNode
= topmostNode
.parentNode
;
10361 // We have no way to detect the $element being attached somewhere without observing the entire
10362 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
10363 // parent node of $element, and instead detect when $element is removed from it (and thus
10364 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
10365 // doesn't get attached, we end up back here and create the parent.
10367 mutationObserver
= new MutationObserver( function ( mutations
) {
10368 var i
, j
, removedNodes
;
10369 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10370 removedNodes
= mutations
[ i
].removedNodes
;
10371 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10372 if ( removedNodes
[ j
] === topmostNode
) {
10373 setTimeout( onRemove
, 0 );
10380 onRemove = function () {
10381 // If the node was attached somewhere else, report it
10382 if ( widget
.isElementAttached() ) {
10383 widget
.onElementAttach();
10385 mutationObserver
.disconnect();
10386 widget
.installParentChangeDetector();
10389 // Create a fake parent and observe it
10390 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10391 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10393 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10394 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10395 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10403 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10404 if ( this.getSaneType( config
) === 'number' ) {
10405 return $( '<input>' )
10406 .attr( 'step', 'any' )
10407 .attr( 'type', 'number' );
10409 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10414 * Get sanitized value for 'type' for given config.
10416 * @param {Object} config Configuration options
10417 * @return {string|null}
10420 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10421 var allowedTypes
= [
10428 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10432 * Focus the input and select a specified range within the text.
10434 * @param {number} from Select from offset
10435 * @param {number} [to] Select to offset, defaults to from
10438 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10439 var isBackwards
, start
, end
,
10440 input
= this.$input
[ 0 ];
10444 isBackwards
= to
< from;
10445 start
= isBackwards
? to
: from;
10446 end
= isBackwards
? from : to
;
10451 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10453 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10454 // Rather than expensively check if the input is attached every time, just check
10455 // if it was the cause of an error being thrown. If not, rethrow the error.
10456 if ( this.getElementDocument().body
.contains( input
) ) {
10464 * Get an object describing the current selection range in a directional manner
10466 * @return {Object} Object containing 'from' and 'to' offsets
10468 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10469 var input
= this.$input
[ 0 ],
10470 start
= input
.selectionStart
,
10471 end
= input
.selectionEnd
,
10472 isBackwards
= input
.selectionDirection
=== 'backward';
10475 from: isBackwards
? end
: start
,
10476 to
: isBackwards
? start
: end
10481 * Get the length of the text input value.
10483 * This could differ from the length of #getValue if the
10484 * value gets filtered
10486 * @return {number} Input length
10488 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10489 return this.$input
[ 0 ].value
.length
;
10493 * Focus the input and select the entire text.
10497 OO
.ui
.TextInputWidget
.prototype.select = function () {
10498 return this.selectRange( 0, this.getInputLength() );
10502 * Focus the input and move the cursor to the start.
10506 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10507 return this.selectRange( 0 );
10511 * Focus the input and move the cursor to the end.
10515 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10516 return this.selectRange( this.getInputLength() );
10520 * Insert new content into the input.
10522 * @param {string} content Content to be inserted
10525 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10527 range
= this.getRange(),
10528 value
= this.getValue();
10530 start
= Math
.min( range
.from, range
.to
);
10531 end
= Math
.max( range
.from, range
.to
);
10533 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10534 this.selectRange( start
+ content
.length
);
10539 * Insert new content either side of a selection.
10541 * @param {string} pre Content to be inserted before the selection
10542 * @param {string} post Content to be inserted after the selection
10545 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10547 range
= this.getRange(),
10548 offset
= pre
.length
;
10550 start
= Math
.min( range
.from, range
.to
);
10551 end
= Math
.max( range
.from, range
.to
);
10553 this.selectRange( start
).insertContent( pre
);
10554 this.selectRange( offset
+ end
).insertContent( post
);
10556 this.selectRange( offset
+ start
, offset
+ end
);
10561 * Set the validation pattern.
10563 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10564 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10565 * value must contain only numbers).
10567 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10568 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10570 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10571 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10572 this.validate
= validate
;
10574 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10579 * Sets the 'invalid' flag appropriately.
10581 * @param {boolean} [isValid] Optionally override validation result
10583 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10585 setFlag = function ( valid
) {
10587 widget
.$input
.attr( 'aria-invalid', 'true' );
10589 widget
.$input
.removeAttr( 'aria-invalid' );
10591 widget
.setFlags( { invalid
: !valid
} );
10594 if ( isValid
!== undefined ) {
10595 setFlag( isValid
);
10597 this.getValidity().then( function () {
10606 * Get the validity of current value.
10608 * This method returns a promise that resolves if the value is valid and rejects if
10609 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10611 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10613 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10616 function rejectOrResolve( valid
) {
10618 return $.Deferred().resolve().promise();
10620 return $.Deferred().reject().promise();
10624 // Check browser validity and reject if it is invalid
10626 this.$input
[ 0 ].checkValidity
!== undefined &&
10627 this.$input
[ 0 ].checkValidity() === false
10629 return rejectOrResolve( false );
10632 // Run our checks if the browser thinks the field is valid
10633 if ( this.validate
instanceof Function
) {
10634 result
= this.validate( this.getValue() );
10635 if ( result
&& $.isFunction( result
.promise
) ) {
10636 return result
.promise().then( function ( valid
) {
10637 return rejectOrResolve( valid
);
10640 return rejectOrResolve( result
);
10643 return rejectOrResolve( this.getValue().match( this.validate
) );
10648 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10650 * @param {string} labelPosition Label position, 'before' or 'after'
10653 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10654 this.labelPosition
= labelPosition
;
10655 if ( this.label
) {
10656 // If there is no label and we only change the position, #updatePosition is a no-op,
10657 // but it takes really a lot of work to do nothing.
10658 this.updatePosition();
10664 * Update the position of the inline label.
10666 * This method is called by #setLabelPosition, and can also be called on its own if
10667 * something causes the label to be mispositioned.
10671 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10672 var after
= this.labelPosition
=== 'after';
10675 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10676 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10678 this.valCache
= null;
10679 this.scrollWidth
= null;
10680 this.positionLabel();
10686 * Position the label by setting the correct padding on the input.
10691 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
10692 var after
, rtl
, property
, newCss
;
10694 if ( this.isWaitingToBeAttached
) {
10695 // #onElementAttach will be called soon, which calls this method
10700 'padding-right': '',
10704 if ( this.label
) {
10705 this.$element
.append( this.$label
);
10707 this.$label
.detach();
10708 // Clear old values if present
10709 this.$input
.css( newCss
);
10713 after
= this.labelPosition
=== 'after';
10714 rtl
= this.$element
.css( 'direction' ) === 'rtl';
10715 property
= after
=== rtl
? 'padding-left' : 'padding-right';
10717 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
10718 // We have to clear the padding on the other side, in case the element direction changed
10719 this.$input
.css( newCss
);
10726 * @extends OO.ui.TextInputWidget
10729 * @param {Object} [config] Configuration options
10731 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
10732 config
= $.extend( {
10736 // Parent constructor
10737 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
10740 this.connect( this, {
10745 this.updateSearchIndicator();
10746 this.connect( this, {
10747 disable
: 'onDisable'
10753 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
10761 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
10768 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10769 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10770 // Clear the text field
10771 this.setValue( '' );
10778 * Update the 'clear' indicator displayed on type: 'search' text
10779 * fields, hiding it when the field is already empty or when it's not
10782 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
10783 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
10784 this.setIndicator( null );
10786 this.setIndicator( 'clear' );
10791 * Handle change events.
10795 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
10796 this.updateSearchIndicator();
10800 * Handle disable events.
10802 * @param {boolean} disabled Element is disabled
10805 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
10806 this.updateSearchIndicator();
10812 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
10813 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
10814 this.updateSearchIndicator();
10820 * @extends OO.ui.TextInputWidget
10823 * @param {Object} [config] Configuration options
10824 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
10825 * specifies minimum number of rows to display.
10826 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
10827 * Use the #maxRows config to specify a maximum number of displayed rows.
10828 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
10829 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
10831 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
10832 config
= $.extend( {
10835 config
.multiline
= false;
10836 // Parent constructor
10837 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
10840 this.multiline
= true;
10841 this.autosize
= !!config
.autosize
;
10842 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
10843 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
10845 // Clone for resizing
10846 if ( this.autosize
) {
10847 this.$clone
= this.$input
10849 .removeAttr( 'id' )
10850 .removeAttr( 'name' )
10851 .insertAfter( this.$input
)
10852 .attr( 'aria-hidden', 'true' )
10853 .addClass( 'oo-ui-element-hidden' );
10857 this.connect( this, {
10862 if ( this.multiline
&& config
.rows
) {
10863 this.$input
.attr( 'rows', config
.rows
);
10865 if ( this.autosize
) {
10866 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
10867 this.isWaitingToBeAttached
= true;
10868 this.installParentChangeDetector();
10874 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
10876 /* Static Methods */
10881 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10882 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10883 state
.scrollTop
= config
.$input
.scrollTop();
10892 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
10893 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
10898 * Handle change events.
10902 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
10909 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
10910 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
10917 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
10919 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
10921 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
10922 // Some platforms emit keycode 10 for ctrl+enter in a textarea
10925 this.emit( 'enter', e
);
10930 * Automatically adjust the size of the text input.
10932 * This only affects multiline inputs that are {@link #autosize autosized}.
10937 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
10938 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
10939 idealHeight
, newHeight
, scrollWidth
, property
;
10941 if ( this.$input
.val() !== this.valCache
) {
10942 if ( this.autosize
) {
10944 .val( this.$input
.val() )
10945 .attr( 'rows', this.minRows
)
10946 // Set inline height property to 0 to measure scroll height
10947 .css( 'height', 0 );
10949 this.$clone
.removeClass( 'oo-ui-element-hidden' );
10951 this.valCache
= this.$input
.val();
10953 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
10955 // Remove inline height property to measure natural heights
10956 this.$clone
.css( 'height', '' );
10957 innerHeight
= this.$clone
.innerHeight();
10958 outerHeight
= this.$clone
.outerHeight();
10960 // Measure max rows height
10962 .attr( 'rows', this.maxRows
)
10963 .css( 'height', 'auto' )
10965 maxInnerHeight
= this.$clone
.innerHeight();
10967 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
10968 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
10969 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
10970 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
10972 this.$clone
.addClass( 'oo-ui-element-hidden' );
10974 // Only apply inline height when expansion beyond natural height is needed
10975 // Use the difference between the inner and outer height as a buffer
10976 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
10977 if ( newHeight
!== this.styleHeight
) {
10978 this.$input
.css( 'height', newHeight
);
10979 this.styleHeight
= newHeight
;
10980 this.emit( 'resize' );
10983 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
10984 if ( scrollWidth
!== this.scrollWidth
) {
10985 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
10987 this.$label
.css( { right
: '', left
: '' } );
10988 this.$indicator
.css( { right
: '', left
: '' } );
10990 if ( scrollWidth
) {
10991 this.$indicator
.css( property
, scrollWidth
);
10992 if ( this.labelPosition
=== 'after' ) {
10993 this.$label
.css( property
, scrollWidth
);
10997 this.scrollWidth
= scrollWidth
;
10998 this.positionLabel();
11008 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11009 return $( '<textarea>' );
11013 * Check if the input supports multiple lines.
11015 * @return {boolean}
11017 OO
.ui
.MultilineTextInputWidget
.prototype.isMultiline = function () {
11018 return !!this.multiline
;
11022 * Check if the input automatically adjusts its size.
11024 * @return {boolean}
11026 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11027 return !!this.autosize
;
11033 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11034 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11035 if ( state
.scrollTop
!== undefined ) {
11036 this.$input
.scrollTop( state
.scrollTop
);
11041 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11042 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11043 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11045 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11046 * option, that option will appear to be selected.
11047 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11050 * After the user chooses an option, its `data` will be used as a new value for the widget.
11051 * A `label` also can be specified for each option: if given, it will be shown instead of the
11052 * `data` in the dropdown menu.
11054 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11056 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
11059 * // Example: A ComboBoxInputWidget.
11060 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11061 * value: 'Option 1',
11063 * { data: 'Option 1' },
11064 * { data: 'Option 2' },
11065 * { data: 'Option 3' }
11068 * $( 'body' ).append( comboBox.$element );
11071 * // Example: A ComboBoxInputWidget with additional option labels.
11072 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11073 * value: 'Option 1',
11076 * data: 'Option 1',
11077 * label: 'Option One'
11080 * data: 'Option 2',
11081 * label: 'Option Two'
11084 * data: 'Option 3',
11085 * label: 'Option Three'
11089 * $( 'body' ).append( comboBox.$element );
11091 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11094 * @extends OO.ui.TextInputWidget
11097 * @param {Object} [config] Configuration options
11098 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11099 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11100 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11101 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11102 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11103 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11105 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11106 // Configuration initialization
11107 config
= $.extend( {
11108 autocomplete
: false
11111 // ComboBoxInputWidget shouldn't support `multiline`
11112 config
.multiline
= false;
11114 // See InputWidget#reusePreInfuseDOM about `config.$input`
11115 if ( config
.$input
) {
11116 config
.$input
.removeAttr( 'list' );
11119 // Parent constructor
11120 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11123 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11124 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11125 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11127 disabled
: this.disabled
11129 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11133 $floatableContainer
: this.$element
,
11134 disabled
: this.isDisabled()
11140 this.connect( this, {
11141 change
: 'onInputChange',
11142 enter
: 'onInputEnter'
11144 this.dropdownButton
.connect( this, {
11145 click
: 'onDropdownButtonClick'
11147 this.menu
.connect( this, {
11148 choose
: 'onMenuChoose',
11149 add
: 'onMenuItemsChange',
11150 remove
: 'onMenuItemsChange',
11151 toggle
: 'onMenuToggle'
11155 this.$input
.attr( {
11157 'aria-owns': this.menu
.getElementId(),
11158 'aria-autocomplete': 'list'
11160 // Do not override options set via config.menu.items
11161 if ( config
.options
!== undefined ) {
11162 this.setOptions( config
.options
);
11164 this.$field
= $( '<div>' )
11165 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11166 .append( this.$input
, this.dropdownButton
.$element
);
11168 .addClass( 'oo-ui-comboBoxInputWidget' )
11169 .append( this.$field
);
11170 this.$overlay
.append( this.menu
.$element
);
11171 this.onMenuItemsChange();
11176 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11181 * Get the combobox's menu.
11183 * @return {OO.ui.MenuSelectWidget} Menu widget
11185 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11190 * Get the combobox's text input widget.
11192 * @return {OO.ui.TextInputWidget} Text input widget
11194 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11199 * Handle input change events.
11202 * @param {string} value New value
11204 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11205 var match
= this.menu
.findItemFromData( value
);
11207 this.menu
.selectItem( match
);
11208 if ( this.menu
.findHighlightedItem() ) {
11209 this.menu
.highlightItem( match
);
11212 if ( !this.isDisabled() ) {
11213 this.menu
.toggle( true );
11218 * Handle input enter events.
11222 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11223 if ( !this.isDisabled() ) {
11224 this.menu
.toggle( false );
11229 * Handle button click events.
11233 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11234 this.menu
.toggle();
11239 * Handle menu choose events.
11242 * @param {OO.ui.OptionWidget} item Chosen item
11244 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11245 this.setValue( item
.getData() );
11249 * Handle menu item change events.
11253 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11254 var match
= this.menu
.findItemFromData( this.getValue() );
11255 this.menu
.selectItem( match
);
11256 if ( this.menu
.findHighlightedItem() ) {
11257 this.menu
.highlightItem( match
);
11259 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11263 * Handle menu toggle events.
11266 * @param {boolean} isVisible Open state of the menu
11268 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11269 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11275 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
11277 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
11279 if ( this.dropdownButton
) {
11280 this.dropdownButton
.setDisabled( this.isDisabled() );
11283 this.menu
.setDisabled( this.isDisabled() );
11290 * Set the options available for this input.
11292 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11295 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11298 .addItems( options
.map( function ( opt
) {
11299 return new OO
.ui
.MenuOptionWidget( {
11301 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11309 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11310 * which is a widget that is specified by reference before any optional configuration settings.
11312 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
11314 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11315 * A left-alignment is used for forms with many fields.
11316 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11317 * A right-alignment is used for long but familiar forms which users tab through,
11318 * verifying the current field with a quick glance at the label.
11319 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11320 * that users fill out from top to bottom.
11321 * - **inline**: The label is placed after the field-widget and aligned to the left.
11322 * An inline-alignment is best used with checkboxes or radio buttons.
11324 * Help text can either be:
11326 * - accessed via a help icon that appears in the upper right corner of the rendered field layout, or
11327 * - shown as a subtle explanation below the label.
11329 * If the help text is brief, or is essential to always espose it, set `helpInline` to `true`. If it
11330 * is long or not essential, leave `helpInline` to its default, `false`.
11332 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11334 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11337 * @extends OO.ui.Layout
11338 * @mixins OO.ui.mixin.LabelElement
11339 * @mixins OO.ui.mixin.TitledElement
11342 * @param {OO.ui.Widget} fieldWidget Field widget
11343 * @param {Object} [config] Configuration options
11344 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11346 * @cfg {Array} [errors] Error messages about the widget, which will be
11347 * displayed below the widget.
11348 * The array may contain strings or OO.ui.HtmlSnippet instances.
11349 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11350 * below the widget.
11351 * The array may contain strings or OO.ui.HtmlSnippet instances.
11352 * These are more visible than `help` messages when `helpInline` is set, and so
11353 * might be good for transient messages.
11354 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11355 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11356 * corner of the rendered field; clicking it will display the text in a popup.
11357 * If `helpInline` is `true`, then a subtle description will be shown after the
11359 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11360 * or shown when the "help" icon is clicked.
11361 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11363 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11365 * @throws {Error} An error is thrown if no widget is specified
11367 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11368 // Allow passing positional parameters inside the config object
11369 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11370 config
= fieldWidget
;
11371 fieldWidget
= config
.fieldWidget
;
11374 // Make sure we have required constructor arguments
11375 if ( fieldWidget
=== undefined ) {
11376 throw new Error( 'Widget not found' );
11379 // Configuration initialization
11380 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11382 // Parent constructor
11383 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11385 // Mixin constructors
11386 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
11387 $label
: $( '<label>' )
11389 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
11392 this.fieldWidget
= fieldWidget
;
11395 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11396 this.$messages
= $( '<ul>' );
11397 this.$header
= $( '<span>' );
11398 this.$body
= $( '<div>' );
11400 this.helpInline
= config
.helpInline
;
11403 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
11406 this.$help
= config
.help
?
11407 this.createHelpElement( config
.help
, config
.$overlay
) :
11409 if ( this.fieldWidget
.getInputId() ) {
11410 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11411 if ( this.helpInline
) {
11412 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
11415 this.$label
.on( 'click', function () {
11416 this.fieldWidget
.simulateLabelClick();
11418 if ( this.helpInline
) {
11419 this.$help
.on( 'click', function () {
11420 this.fieldWidget
.simulateLabelClick();
11425 .addClass( 'oo-ui-fieldLayout' )
11426 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11427 .append( this.$body
);
11428 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11429 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11430 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11432 .addClass( 'oo-ui-fieldLayout-field' )
11433 .append( this.fieldWidget
.$element
);
11435 this.setErrors( config
.errors
|| [] );
11436 this.setNotices( config
.notices
|| [] );
11437 this.setAlignment( config
.align
);
11438 // Call this again to take into account the widget's accessKey
11439 this.updateTitle();
11444 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11445 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11446 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11451 * Handle field disable events.
11454 * @param {boolean} value Field is disabled
11456 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11457 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11461 * Get the widget contained by the field.
11463 * @return {OO.ui.Widget} Field widget
11465 OO
.ui
.FieldLayout
.prototype.getField = function () {
11466 return this.fieldWidget
;
11470 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11471 * #setAlignment). Return `false` if it can't or if this can't be determined.
11473 * @return {boolean}
11475 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11476 // This is very simplistic, but should be good enough.
11477 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11482 * @param {string} kind 'error' or 'notice'
11483 * @param {string|OO.ui.HtmlSnippet} text
11486 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11487 var $listItem
, $icon
, message
;
11488 $listItem
= $( '<li>' );
11489 if ( kind
=== 'error' ) {
11490 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11491 $listItem
.attr( 'role', 'alert' );
11492 } else if ( kind
=== 'notice' ) {
11493 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
11497 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11499 .append( $icon
, message
.$element
)
11500 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11505 * Set the field alignment mode.
11508 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11511 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11512 if ( value
!== this.align
) {
11513 // Default to 'left'
11514 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11518 if ( value
=== 'inline' && !this.isFieldInline() ) {
11521 // Reorder elements
11523 if ( this.helpInline
) {
11524 if ( value
=== 'inline' ) {
11525 this.$header
.append( this.$label
, this.$help
);
11526 this.$body
.append( this.$field
, this.$header
);
11528 this.$header
.append( this.$label
, this.$help
);
11529 this.$body
.append( this.$header
, this.$field
);
11532 if ( value
=== 'top' ) {
11533 this.$header
.append( this.$help
, this.$label
);
11534 this.$body
.append( this.$header
, this.$field
);
11535 } else if ( value
=== 'inline' ) {
11536 this.$header
.append( this.$help
, this.$label
);
11537 this.$body
.append( this.$field
, this.$header
);
11539 this.$header
.append( this.$label
);
11540 this.$body
.append( this.$header
, this.$help
, this.$field
);
11543 // Set classes. The following classes can be used here:
11544 // * oo-ui-fieldLayout-align-left
11545 // * oo-ui-fieldLayout-align-right
11546 // * oo-ui-fieldLayout-align-top
11547 // * oo-ui-fieldLayout-align-inline
11548 if ( this.align
) {
11549 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11551 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11552 this.align
= value
;
11559 * Set the list of error messages.
11561 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11562 * The array may contain strings or OO.ui.HtmlSnippet instances.
11565 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11566 this.errors
= errors
.slice();
11567 this.updateMessages();
11572 * Set the list of notice messages.
11574 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11575 * The array may contain strings or OO.ui.HtmlSnippet instances.
11578 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11579 this.notices
= notices
.slice();
11580 this.updateMessages();
11585 * Update the rendering of error and notice messages.
11589 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11591 this.$messages
.empty();
11593 if ( this.errors
.length
|| this.notices
.length
) {
11594 this.$body
.after( this.$messages
);
11596 this.$messages
.remove();
11600 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11601 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11603 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11604 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11609 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11610 * (This is a bit of a hack.)
11613 * @param {string} title Tooltip label for 'title' attribute
11616 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11617 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11618 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11624 * Creates and returns the help element. Also sets the `aria-describedby`
11625 * attribute on the main element of the `fieldWidget`.
11628 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
11629 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
11630 * @return {jQuery} The element that should become `this.$help`.
11632 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
11633 var helpId
, helpWidget
;
11635 if ( this.helpInline
) {
11636 helpWidget
= new OO
.ui
.LabelWidget( {
11638 classes
: [ 'oo-ui-inline-help' ]
11641 helpId
= helpWidget
.getElementId();
11643 helpWidget
= new OO
.ui
.PopupButtonWidget( {
11644 $overlay
: $overlay
,
11648 classes
: [ 'oo-ui-fieldLayout-help' ],
11651 label
: OO
.ui
.msg( 'ooui-field-help' )
11653 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
11654 helpWidget
.getPopup().$body
.html( help
.toString() );
11656 helpWidget
.getPopup().$body
.text( help
);
11659 helpId
= helpWidget
.getPopup().getBodyId();
11662 // Set the 'aria-describedby' attribute on the fieldWidget
11663 // Preference given to an input or a button
11665 this.fieldWidget
.$input
||
11666 this.fieldWidget
.$button
||
11667 this.fieldWidget
.$element
11668 ).attr( 'aria-describedby', helpId
);
11670 return helpWidget
.$element
;
11674 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
11675 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
11676 * is required and is specified before any optional configuration settings.
11678 * Labels can be aligned in one of four ways:
11680 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11681 * A left-alignment is used for forms with many fields.
11682 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11683 * A right-alignment is used for long but familiar forms which users tab through,
11684 * verifying the current field with a quick glance at the label.
11685 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11686 * that users fill out from top to bottom.
11687 * - **inline**: The label is placed after the field-widget and aligned to the left.
11688 * An inline-alignment is best used with checkboxes or radio buttons.
11690 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
11691 * text is specified.
11694 * // Example of an ActionFieldLayout
11695 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
11696 * new OO.ui.TextInputWidget( {
11697 * placeholder: 'Field widget'
11699 * new OO.ui.ButtonWidget( {
11703 * label: 'An ActionFieldLayout. This label is aligned top',
11705 * help: 'This is help text'
11709 * $( 'body' ).append( actionFieldLayout.$element );
11712 * @extends OO.ui.FieldLayout
11715 * @param {OO.ui.Widget} fieldWidget Field widget
11716 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
11717 * @param {Object} config
11719 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
11720 // Allow passing positional parameters inside the config object
11721 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11722 config
= fieldWidget
;
11723 fieldWidget
= config
.fieldWidget
;
11724 buttonWidget
= config
.buttonWidget
;
11727 // Parent constructor
11728 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
11731 this.buttonWidget
= buttonWidget
;
11732 this.$button
= $( '<span>' );
11733 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11737 .addClass( 'oo-ui-actionFieldLayout' );
11739 .addClass( 'oo-ui-actionFieldLayout-button' )
11740 .append( this.buttonWidget
.$element
);
11742 .addClass( 'oo-ui-actionFieldLayout-input' )
11743 .append( this.fieldWidget
.$element
);
11745 .append( this.$input
, this.$button
);
11750 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
11753 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
11754 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
11755 * configured with a label as well. For more information and examples,
11756 * please see the [OOUI documentation on MediaWiki][1].
11759 * // Example of a fieldset layout
11760 * var input1 = new OO.ui.TextInputWidget( {
11761 * placeholder: 'A text input field'
11764 * var input2 = new OO.ui.TextInputWidget( {
11765 * placeholder: 'A text input field'
11768 * var fieldset = new OO.ui.FieldsetLayout( {
11769 * label: 'Example of a fieldset layout'
11772 * fieldset.addItems( [
11773 * new OO.ui.FieldLayout( input1, {
11774 * label: 'Field One'
11776 * new OO.ui.FieldLayout( input2, {
11777 * label: 'Field Two'
11780 * $( 'body' ).append( fieldset.$element );
11782 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11785 * @extends OO.ui.Layout
11786 * @mixins OO.ui.mixin.IconElement
11787 * @mixins OO.ui.mixin.LabelElement
11788 * @mixins OO.ui.mixin.GroupElement
11791 * @param {Object} [config] Configuration options
11792 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
11793 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
11794 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
11795 * For important messages, you are advised to use `notices`, as they are always shown.
11796 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
11797 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11799 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
11800 // Configuration initialization
11801 config
= config
|| {};
11803 // Parent constructor
11804 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
11806 // Mixin constructors
11807 OO
.ui
.mixin
.IconElement
.call( this, config
);
11808 OO
.ui
.mixin
.LabelElement
.call( this, config
);
11809 OO
.ui
.mixin
.GroupElement
.call( this, config
);
11812 this.$header
= $( '<legend>' );
11813 if ( config
.help
) {
11814 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
11815 $overlay
: config
.$overlay
,
11819 classes
: [ 'oo-ui-fieldsetLayout-help' ],
11822 label
: OO
.ui
.msg( 'ooui-field-help' )
11824 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
11825 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
11827 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
11829 this.$help
= this.popupButtonWidget
.$element
;
11831 this.$help
= $( [] );
11836 .addClass( 'oo-ui-fieldsetLayout-header' )
11837 .append( this.$icon
, this.$label
, this.$help
);
11838 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
11840 .addClass( 'oo-ui-fieldsetLayout' )
11841 .prepend( this.$header
, this.$group
);
11842 if ( Array
.isArray( config
.items
) ) {
11843 this.addItems( config
.items
);
11849 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
11850 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
11851 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
11852 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
11854 /* Static Properties */
11860 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
11863 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
11864 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
11865 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
11866 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
11868 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
11869 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
11870 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
11871 * some fancier controls. Some controls have both regular and InputWidget variants, for example
11872 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
11873 * often have simplified APIs to match the capabilities of HTML forms.
11874 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
11876 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
11877 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
11880 * // Example of a form layout that wraps a fieldset layout
11881 * var input1 = new OO.ui.TextInputWidget( {
11882 * placeholder: 'Username'
11884 * var input2 = new OO.ui.TextInputWidget( {
11885 * placeholder: 'Password',
11888 * var submit = new OO.ui.ButtonInputWidget( {
11892 * var fieldset = new OO.ui.FieldsetLayout( {
11893 * label: 'A form layout'
11895 * fieldset.addItems( [
11896 * new OO.ui.FieldLayout( input1, {
11897 * label: 'Username',
11900 * new OO.ui.FieldLayout( input2, {
11901 * label: 'Password',
11904 * new OO.ui.FieldLayout( submit )
11906 * var form = new OO.ui.FormLayout( {
11907 * items: [ fieldset ],
11908 * action: '/api/formhandler',
11911 * $( 'body' ).append( form.$element );
11914 * @extends OO.ui.Layout
11915 * @mixins OO.ui.mixin.GroupElement
11918 * @param {Object} [config] Configuration options
11919 * @cfg {string} [method] HTML form `method` attribute
11920 * @cfg {string} [action] HTML form `action` attribute
11921 * @cfg {string} [enctype] HTML form `enctype` attribute
11922 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
11924 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
11927 // Configuration initialization
11928 config
= config
|| {};
11930 // Parent constructor
11931 OO
.ui
.FormLayout
.parent
.call( this, config
);
11933 // Mixin constructors
11934 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
11937 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
11939 // Make sure the action is safe
11940 action
= config
.action
;
11941 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
11942 action
= './' + action
;
11947 .addClass( 'oo-ui-formLayout' )
11949 method
: config
.method
,
11951 enctype
: config
.enctype
11953 if ( Array
.isArray( config
.items
) ) {
11954 this.addItems( config
.items
);
11960 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
11961 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
11966 * A 'submit' event is emitted when the form is submitted.
11971 /* Static Properties */
11977 OO
.ui
.FormLayout
.static.tagName
= 'form';
11982 * Handle form submit events.
11985 * @param {jQuery.Event} e Submit event
11988 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
11989 if ( this.emit( 'submit' ) ) {
11995 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
11996 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
11999 * // Example of a panel layout
12000 * var panel = new OO.ui.PanelLayout( {
12004 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12006 * $( 'body' ).append( panel.$element );
12009 * @extends OO.ui.Layout
12012 * @param {Object} [config] Configuration options
12013 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12014 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12015 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12016 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
12018 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12019 // Configuration initialization
12020 config
= $.extend( {
12027 // Parent constructor
12028 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12031 this.$element
.addClass( 'oo-ui-panelLayout' );
12032 if ( config
.scrollable
) {
12033 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12035 if ( config
.padded
) {
12036 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12038 if ( config
.expanded
) {
12039 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12041 if ( config
.framed
) {
12042 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12048 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12053 * Focus the panel layout
12055 * The default implementation just focuses the first focusable element in the panel
12057 OO
.ui
.PanelLayout
.prototype.focus = function () {
12058 OO
.ui
.findFocusable( this.$element
).focus();
12062 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12063 * items), with small margins between them. Convenient when you need to put a number of block-level
12064 * widgets on a single line next to each other.
12066 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12069 * // HorizontalLayout with a text input and a label
12070 * var layout = new OO.ui.HorizontalLayout( {
12072 * new OO.ui.LabelWidget( { label: 'Label' } ),
12073 * new OO.ui.TextInputWidget( { value: 'Text' } )
12076 * $( 'body' ).append( layout.$element );
12079 * @extends OO.ui.Layout
12080 * @mixins OO.ui.mixin.GroupElement
12083 * @param {Object} [config] Configuration options
12084 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12086 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12087 // Configuration initialization
12088 config
= config
|| {};
12090 // Parent constructor
12091 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12093 // Mixin constructors
12094 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12097 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12098 if ( Array
.isArray( config
.items
) ) {
12099 this.addItems( config
.items
);
12105 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12106 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12109 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12110 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12111 * (to adjust the value in increments) to allow the user to enter a number.
12114 * // Example: A NumberInputWidget.
12115 * var numberInput = new OO.ui.NumberInputWidget( {
12116 * label: 'NumberInputWidget',
12117 * input: { value: 5 },
12121 * $( 'body' ).append( numberInput.$element );
12124 * @extends OO.ui.TextInputWidget
12127 * @param {Object} [config] Configuration options
12128 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
12129 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
12130 * @cfg {boolean} [allowInteger=false] Whether the field accepts only integer values.
12131 * @cfg {number} [min=-Infinity] Minimum allowed value
12132 * @cfg {number} [max=Infinity] Maximum allowed value
12133 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
12134 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
12135 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12137 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12138 var $field
= $( '<div>' )
12139 .addClass( 'oo-ui-numberInputWidget-field' );
12141 // Configuration initialization
12142 config
= $.extend( {
12143 allowInteger
: false,
12151 // For backward compatibility
12152 $.extend( config
, config
.input
);
12155 // Parent constructor
12156 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12160 if ( config
.showButtons
) {
12161 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12163 disabled
: this.isDisabled(),
12165 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12170 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12171 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12173 disabled
: this.isDisabled(),
12175 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12180 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12185 keydown
: this.onKeyDown
.bind( this ),
12186 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12188 if ( config
.showButtons
) {
12189 this.plusButton
.connect( this, {
12190 click
: [ 'onButtonClick', +1 ]
12192 this.minusButton
.connect( this, {
12193 click
: [ 'onButtonClick', -1 ]
12198 $field
.append( this.$input
);
12199 if ( config
.showButtons
) {
12201 .prepend( this.minusButton
.$element
)
12202 .append( this.plusButton
.$element
);
12206 this.setAllowInteger( config
.allowInteger
|| config
.isInteger
);
12207 this.setRange( config
.min
, config
.max
);
12208 this.setStep( config
.step
, config
.pageStep
);
12209 // Set the validation method after we set allowInteger and range
12210 // so that it doesn't immediately call setValidityFlag
12211 this.setValidation( this.validateNumber
.bind( this ) );
12214 .addClass( 'oo-ui-numberInputWidget' )
12215 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12221 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12226 * Set whether only integers are allowed
12228 * @param {boolean} flag
12230 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12231 this.allowInteger
= !!flag
;
12232 this.setValidityFlag();
12234 // Backward compatibility
12235 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12238 * Get whether only integers are allowed
12240 * @return {boolean} Flag value
12242 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12243 return this.allowInteger
;
12245 // Backward compatibility
12246 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12249 * Set the range of allowed values
12251 * @param {number} min Minimum allowed value
12252 * @param {number} max Maximum allowed value
12254 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12256 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12260 this.$input
.attr( 'min', this.min
);
12261 this.$input
.attr( 'max', this.max
);
12262 this.setValidityFlag();
12266 * Get the current range
12268 * @return {number[]} Minimum and maximum values
12270 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12271 return [ this.min
, this.max
];
12275 * Set the stepping deltas
12277 * @param {number} step Normal step
12278 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
12280 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( step
, pageStep
) {
12282 throw new Error( 'Step value must be positive' );
12284 if ( pageStep
=== null ) {
12285 pageStep
= step
* 10;
12286 } else if ( pageStep
<= 0 ) {
12287 throw new Error( 'Page step value must be positive' );
12290 this.pageStep
= pageStep
;
12291 this.$input
.attr( 'step', this.step
);
12297 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12298 if ( value
=== '' ) {
12299 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12300 // so here we make sure an 'empty' value is actually displayed as such.
12301 this.$input
.val( '' );
12303 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12307 * Get the current stepping values
12309 * @return {number[]} Step and page step
12311 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12312 return [ this.step
, this.pageStep
];
12316 * Get the current value of the widget as a number
12318 * @return {number} May be NaN, or an invalid number
12320 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12321 return +this.getValue();
12325 * Adjust the value of the widget
12327 * @param {number} delta Adjustment amount
12329 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12330 var n
, v
= this.getNumericValue();
12333 if ( isNaN( delta
) || !isFinite( delta
) ) {
12334 throw new Error( 'Delta must be a finite number' );
12337 if ( isNaN( v
) ) {
12341 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12342 if ( this.allowInteger
) {
12343 n
= Math
.round( n
);
12348 this.setValue( n
);
12355 * @param {string} value Field value
12356 * @return {boolean}
12358 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12360 if ( value
=== '' ) {
12361 return !this.isRequired();
12364 if ( isNaN( n
) || !isFinite( n
) ) {
12368 if ( this.allowInteger
&& Math
.floor( n
) !== n
) {
12372 if ( n
< this.min
|| n
> this.max
) {
12380 * Handle mouse click events.
12383 * @param {number} dir +1 or -1
12385 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
12386 this.adjustValue( dir
* this.step
);
12390 * Handle mouse wheel events.
12393 * @param {jQuery.Event} event
12395 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
12398 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
12399 // Standard 'wheel' event
12400 if ( event
.originalEvent
.deltaMode
!== undefined ) {
12401 this.sawWheelEvent
= true;
12403 if ( event
.originalEvent
.deltaY
) {
12404 delta
= -event
.originalEvent
.deltaY
;
12405 } else if ( event
.originalEvent
.deltaX
) {
12406 delta
= event
.originalEvent
.deltaX
;
12409 // Non-standard events
12410 if ( !this.sawWheelEvent
) {
12411 if ( event
.originalEvent
.wheelDeltaX
) {
12412 delta
= -event
.originalEvent
.wheelDeltaX
;
12413 } else if ( event
.originalEvent
.wheelDeltaY
) {
12414 delta
= event
.originalEvent
.wheelDeltaY
;
12415 } else if ( event
.originalEvent
.wheelDelta
) {
12416 delta
= event
.originalEvent
.wheelDelta
;
12417 } else if ( event
.originalEvent
.detail
) {
12418 delta
= -event
.originalEvent
.detail
;
12423 delta
= delta
< 0 ? -1 : 1;
12424 this.adjustValue( delta
* this.step
);
12432 * Handle key down events.
12435 * @param {jQuery.Event} e Key down event
12437 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
12438 if ( !this.isDisabled() ) {
12439 switch ( e
.which
) {
12440 case OO
.ui
.Keys
.UP
:
12441 this.adjustValue( this.step
);
12443 case OO
.ui
.Keys
.DOWN
:
12444 this.adjustValue( -this.step
);
12446 case OO
.ui
.Keys
.PAGEUP
:
12447 this.adjustValue( this.pageStep
);
12449 case OO
.ui
.Keys
.PAGEDOWN
:
12450 this.adjustValue( -this.pageStep
);
12459 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
12461 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
12463 if ( this.minusButton
) {
12464 this.minusButton
.setDisabled( this.isDisabled() );
12466 if ( this.plusButton
) {
12467 this.plusButton
.setDisabled( this.isDisabled() );
12475 //# sourceMappingURL=oojs-ui-core.js.map.json