3 * https://www.mediawiki.org/wiki/OOUI
5 * Copyright 2011–2019 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2019-04-04T19:10:48Z
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,
215 * otherwise only match descendants
216 * @return {boolean} The node is in the list of target nodes
218 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
220 if ( !Array
.isArray( containers
) ) {
221 containers
= [ containers
];
223 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
225 ( matchContainers
&& contained
=== containers
[ i
] ) ||
226 $.contains( containers
[ i
], contained
)
235 * Return a function, that, as long as it continues to be invoked, will not
236 * be triggered. The function will be called after it stops being called for
237 * N milliseconds. If `immediate` is passed, trigger the function on the
238 * leading edge, instead of the trailing.
240 * Ported from: http://underscorejs.org/underscore.js
242 * @param {Function} func Function to debounce
243 * @param {number} [wait=0] Wait period in milliseconds
244 * @param {boolean} [immediate] Trigger on leading edge
245 * @return {Function} Debounced function
247 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
252 later = function () {
255 func
.apply( context
, args
);
258 if ( immediate
&& !timeout
) {
259 func
.apply( context
, args
);
261 if ( !timeout
|| wait
) {
262 clearTimeout( timeout
);
263 timeout
= setTimeout( later
, wait
);
269 * Puts a console warning with provided message.
271 * @param {string} message Message
273 OO
.ui
.warnDeprecation = function ( message
) {
274 if ( OO
.getProp( window
, 'console', 'warn' ) !== undefined ) {
275 // eslint-disable-next-line no-console
276 console
.warn( message
);
281 * Returns a function, that, when invoked, will only be triggered at most once
282 * during a given window of time. If called again during that window, it will
283 * wait until the window ends and then trigger itself again.
285 * As it's not knowable to the caller whether the function will actually run
286 * when the wrapper is called, return values from the function are entirely
289 * @param {Function} func Function to throttle
290 * @param {number} wait Throttle window length, in milliseconds
291 * @return {Function} Throttled function
293 OO
.ui
.throttle = function ( func
, wait
) {
294 var context
, args
, timeout
,
298 previous
= Date
.now();
299 func
.apply( context
, args
);
302 // Check how long it's been since the last time the function was
303 // called, and whether it's more or less than the requested throttle
304 // period. If it's less, run the function immediately. If it's more,
305 // set a timeout for the remaining time -- but don't replace an
306 // existing timeout, since that'd indefinitely prolong the wait.
307 var remaining
= wait
- ( Date
.now() - previous
);
310 if ( remaining
<= 0 ) {
311 // Note: unless wait was ridiculously large, this means we'll
312 // automatically run the first time the function was called in a
313 // given period. (If you provide a wait period larger than the
314 // current Unix timestamp, you *deserve* unexpected behavior.)
315 clearTimeout( timeout
);
317 } else if ( !timeout
) {
318 timeout
= setTimeout( run
, remaining
);
324 * A (possibly faster) way to get the current timestamp as an integer.
326 * @deprecated Since 0.31.1; use `Date.now()` instead.
327 * @return {number} Current timestamp, in milliseconds since the Unix epoch
329 OO
.ui
.now = function () {
330 OO
.ui
.warnDeprecation( 'OO.ui.now() is deprecated, use Date.now() instead' );
335 * Reconstitute a JavaScript object corresponding to a widget created by
336 * the PHP implementation.
338 * This is an alias for `OO.ui.Element.static.infuse()`.
340 * @param {string|HTMLElement|jQuery} idOrNode
341 * A DOM id (if a string) or node for the widget to infuse.
342 * @param {Object} [config] Configuration options
343 * @return {OO.ui.Element}
344 * The `OO.ui.Element` corresponding to this (infusable) document node.
346 OO
.ui
.infuse = function ( idOrNode
, config
) {
347 return OO
.ui
.Element
.static.infuse( idOrNode
, config
);
351 * Get a localized message.
353 * After the message key, message parameters may optionally be passed. In the default
354 * implementation, any occurrences of $1 are replaced with the first parameter, $2 with the
355 * second parameter, etc.
356 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long
357 * as they support unnamed, ordered message parameters.
359 * In environments that provide a localization system, this function should be overridden to
360 * return the message translated in the user's language. The default implementation always
361 * returns English messages. An example of doing this with
362 * [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) follows.
365 * var i, iLen, button,
366 * messagePath = 'oojs-ui/dist/i18n/',
367 * languages = [ $.i18n().locale, 'ur', 'en' ],
370 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
371 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
374 * $.i18n().load( languageMap ).done( function() {
375 * // Replace the built-in `msg` only once we've loaded the internationalization.
376 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
377 * // you put off creating any widgets until this promise is complete, no English
378 * // will be displayed.
379 * OO.ui.msg = $.i18n;
381 * // A button displaying "OK" in the default locale
382 * button = new OO.ui.ButtonWidget( {
383 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
386 * $( document.body ).append( button.$element );
388 * // A button displaying "OK" in Urdu
389 * $.i18n().locale = 'ur';
390 * button = new OO.ui.ButtonWidget( {
391 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
394 * $( document.body ).append( button.$element );
397 * @param {string} key Message key
398 * @param {...Mixed} [params] Message parameters
399 * @return {string} Translated message with parameters substituted
401 OO
.ui
.msg = function ( key
) {
402 // `OO.ui.msg.messages` is defined in code generated during the build process
403 var messages
= OO
.ui
.msg
.messages
,
404 message
= messages
[ key
],
405 params
= Array
.prototype.slice
.call( arguments
, 1 );
406 if ( typeof message
=== 'string' ) {
407 // Perform $1 substitution
408 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
409 var i
= parseInt( n
, 10 );
410 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
413 // Return placeholder if message not found
414 message
= '[' + key
+ ']';
420 * Package a message and arguments for deferred resolution.
422 * Use this when you are statically specifying a message and the message may not yet be present.
424 * @param {string} key Message key
425 * @param {...Mixed} [params] Message parameters
426 * @return {Function} Function that returns the resolved message when executed
428 OO
.ui
.deferMsg = function () {
429 var args
= arguments
;
431 return OO
.ui
.msg
.apply( OO
.ui
, args
);
438 * If the message is a function it will be executed, otherwise it will pass through directly.
440 * @param {Function|string} msg Deferred message, or message text
441 * @return {string} Resolved message
443 OO
.ui
.resolveMsg = function ( msg
) {
444 if ( typeof msg
=== 'function' ) {
451 * @param {string} url
454 OO
.ui
.isSafeUrl = function ( url
) {
455 // Keep this function in sync with php/Tag.php
456 var i
, protocolWhitelist
;
458 function stringStartsWith( haystack
, needle
) {
459 return haystack
.substr( 0, needle
.length
) === needle
;
462 protocolWhitelist
= [
463 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
464 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
465 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
472 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
473 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
478 // This matches '//' too
479 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
482 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
490 * Check if the user has a 'mobile' device.
492 * For our purposes this means the user is primarily using an
493 * on-screen keyboard, touch input instead of a mouse and may
494 * have a physically small display.
496 * It is left up to implementors to decide how to compute this
497 * so the default implementation always returns false.
499 * @return {boolean} User is on a mobile device
501 OO
.ui
.isMobile = function () {
506 * Get the additional spacing that should be taken into account when displaying elements that are
507 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
508 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
510 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
511 * the extra spacing from that edge of viewport (in pixels)
513 OO
.ui
.getViewportSpacing = function () {
523 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
524 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
526 * @return {jQuery} Default overlay node
528 OO
.ui
.getDefaultOverlay = function () {
529 if ( !OO
.ui
.$defaultOverlay
) {
530 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
531 $( document
.body
).append( OO
.ui
.$defaultOverlay
);
533 return OO
.ui
.$defaultOverlay
;
537 * Message store for the default implementation of OO.ui.msg.
539 * Environments that provide a localization system should not use this, but should override
540 * OO.ui.msg altogether.
544 OO
.ui
.msg
.messages
= {
545 "ooui-outline-control-move-down": "Move item down",
546 "ooui-outline-control-move-up": "Move item up",
547 "ooui-outline-control-remove": "Remove item",
548 "ooui-toolbar-more": "More",
549 "ooui-toolgroup-expand": "More",
550 "ooui-toolgroup-collapse": "Fewer",
551 "ooui-item-remove": "Remove",
552 "ooui-dialog-message-accept": "OK",
553 "ooui-dialog-message-reject": "Cancel",
554 "ooui-dialog-process-error": "Something went wrong",
555 "ooui-dialog-process-dismiss": "Dismiss",
556 "ooui-dialog-process-retry": "Try again",
557 "ooui-dialog-process-continue": "Continue",
558 "ooui-combobox-button-label": "Dropdown for combobox",
559 "ooui-selectfile-button-select": "Select a file",
560 "ooui-selectfile-not-supported": "File selection is not supported",
561 "ooui-selectfile-placeholder": "No file is selected",
562 "ooui-selectfile-dragdrop-placeholder": "Drop file here",
563 "ooui-field-help": "Help"
571 * Namespace for OOUI mixins.
573 * Mixins are named according to the type of object they are intended to
574 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
575 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
576 * is intended to be mixed in to an instance of OO.ui.Widget.
584 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
585 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not
586 * have events connected to them and can't be interacted with.
592 * @param {Object} [config] Configuration options
593 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are
594 * added to the top level (e.g., the outermost div) of the element. See the
595 * [OOUI documentation on MediaWiki][2] for an example.
596 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
597 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
598 * @cfg {string} [text] Text to insert
599 * @cfg {Array} [content] An array of content elements to append (after #text).
600 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
601 * Instances of OO.ui.Element will have their $element appended.
602 * @cfg {jQuery} [$content] Content elements to append (after #text).
603 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
604 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number,
606 * Data can also be specified with the #setData method.
608 OO
.ui
.Element
= function OoUiElement( config
) {
609 if ( OO
.ui
.isDemo
) {
610 this.initialConfig
= config
;
612 // Configuration initialization
613 config
= config
|| {};
616 this.$ = function () {
617 OO
.ui
.warnDeprecation( 'this.$ is deprecated, use global $ instead' );
618 return $.apply( this, arguments
);
620 this.elementId
= null;
622 this.data
= config
.data
;
623 this.$element
= config
.$element
||
624 $( document
.createElement( this.getTagName() ) );
625 this.elementGroup
= null;
628 if ( Array
.isArray( config
.classes
) ) {
629 this.$element
.addClass( config
.classes
);
632 this.setElementId( config
.id
);
635 this.$element
.text( config
.text
);
637 if ( config
.content
) {
638 // The `content` property treats plain strings as text; use an
639 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
640 // appropriate $element appended.
641 this.$element
.append( config
.content
.map( function ( v
) {
642 if ( typeof v
=== 'string' ) {
643 // Escape string so it is properly represented in HTML.
644 // Don't create empty text nodes for empty strings.
645 return v
? document
.createTextNode( v
) : undefined;
646 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
649 } else if ( v
instanceof OO
.ui
.Element
) {
655 if ( config
.$content
) {
656 // The `$content` property treats plain strings as HTML.
657 this.$element
.append( config
.$content
);
663 OO
.initClass( OO
.ui
.Element
);
665 /* Static Properties */
668 * The name of the HTML tag used by the element.
670 * The static value may be ignored if the #getTagName method is overridden.
676 OO
.ui
.Element
.static.tagName
= 'div';
681 * Reconstitute a JavaScript object corresponding to a widget created
682 * by the PHP implementation.
684 * @param {string|HTMLElement|jQuery} idOrNode
685 * A DOM id (if a string) or node for the widget to infuse.
686 * @param {Object} [config] Configuration options
687 * @return {OO.ui.Element}
688 * The `OO.ui.Element` corresponding to this (infusable) document node.
689 * For `Tag` objects emitted on the HTML side (used occasionally for content)
690 * the value returned is a newly-created Element wrapping around the existing
693 OO
.ui
.Element
.static.infuse = function ( idOrNode
, config
) {
694 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, config
, false );
696 if ( typeof idOrNode
=== 'string' ) {
697 // IDs deprecated since 0.29.7
698 OO
.ui
.warnDeprecation(
699 'Passing a string ID to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
702 // Verify that the type matches up.
703 // FIXME: uncomment after T89721 is fixed, see T90929.
705 if ( !( obj instanceof this['class'] ) ) {
706 throw new Error( 'Infusion type mismatch!' );
713 * Implementation helper for `infuse`; skips the type check and has an
714 * extra property so that only the top-level invocation touches the DOM.
717 * @param {string|HTMLElement|jQuery} idOrNode
718 * @param {Object} [config] Configuration options
719 * @param {jQuery.Promise} [domPromise] A promise that will be resolved
720 * when the top-level widget of this infusion is inserted into DOM,
721 * replacing the original node; only used internally.
722 * @return {OO.ui.Element}
724 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, config
, domPromise
) {
725 // look for a cached result of a previous infusion.
726 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
727 if ( typeof idOrNode
=== 'string' ) {
729 $elem
= $( document
.getElementById( id
) );
731 $elem
= $( idOrNode
);
732 id
= $elem
.attr( 'id' );
734 if ( !$elem
.length
) {
735 if ( typeof idOrNode
=== 'string' ) {
736 error
= 'Widget not found: ' + idOrNode
;
737 } else if ( idOrNode
&& idOrNode
.selector
) {
738 error
= 'Widget not found: ' + idOrNode
.selector
;
740 error
= 'Widget not found';
742 throw new Error( error
);
744 if ( $elem
[ 0 ].oouiInfused
) {
745 $elem
= $elem
[ 0 ].oouiInfused
;
747 data
= $elem
.data( 'ooui-infused' );
750 if ( data
=== true ) {
751 throw new Error( 'Circular dependency! ' + id
);
754 // Pick up dynamic state, like focus, value of form inputs, scroll position, etc.
755 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
756 // Restore dynamic state after the new element is re-inserted into DOM under
758 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
759 infusedChildren
= $elem
.data( 'ooui-infused-children' );
760 if ( infusedChildren
&& infusedChildren
.length
) {
761 infusedChildren
.forEach( function ( data
) {
762 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
763 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
769 data
= $elem
.attr( 'data-ooui' );
771 throw new Error( 'No infusion data found: ' + id
);
774 data
= JSON
.parse( data
);
778 if ( !( data
&& data
._
) ) {
779 throw new Error( 'No valid infusion data found: ' + id
);
781 if ( data
._
=== 'Tag' ) {
782 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
783 return new OO
.ui
.Element( $.extend( {}, config
, { $element
: $elem
} ) );
785 parts
= data
._
.split( '.' );
786 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
787 if ( cls
=== undefined ) {
788 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
791 // Verify that we're creating an OO.ui.Element instance
794 while ( parent
!== undefined ) {
795 if ( parent
=== OO
.ui
.Element
) {
800 parent
= parent
.parent
;
803 if ( parent
!== OO
.ui
.Element
) {
804 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
809 domPromise
= top
.promise();
811 $elem
.data( 'ooui-infused', true ); // prevent loops
812 data
.id
= id
; // implicit
813 infusedChildren
= [];
814 data
= OO
.copy( data
, null, function deserialize( value
) {
816 if ( OO
.isPlainObject( value
) ) {
818 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, config
, domPromise
);
819 infusedChildren
.push( infused
);
820 // Flatten the structure
821 infusedChildren
.push
.apply(
823 infused
.$element
.data( 'ooui-infused-children' ) || []
825 infused
.$element
.removeData( 'ooui-infused-children' );
828 if ( value
.html
!== undefined ) {
829 return new OO
.ui
.HtmlSnippet( value
.html
);
833 // allow widgets to reuse parts of the DOM
834 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
835 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
836 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
838 // eslint-disable-next-line new-cap
839 obj
= new cls( $.extend( {}, config
, data
) );
840 // If anyone is holding a reference to the old DOM element,
841 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
842 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
843 $elem
[ 0 ].oouiInfused
= obj
.$element
;
844 // now replace old DOM with this new DOM.
846 // An efficient constructor might be able to reuse the entire DOM tree of the original
847 // element, so only mutate the DOM if we need to.
848 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
849 $elem
.replaceWith( obj
.$element
);
853 obj
.$element
.data( 'ooui-infused', obj
);
854 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
855 // set the 'data-ooui' attribute so we can identify infused widgets
856 obj
.$element
.attr( 'data-ooui', '' );
857 // restore dynamic state after the new element is inserted into DOM
858 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
863 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
865 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
866 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
867 * constructor, which will be given the enhanced config.
870 * @param {HTMLElement} node
871 * @param {Object} config
874 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
879 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM
880 * node (and its children) that represent an Element of the same class and the given configuration,
881 * generated by the PHP implementation.
883 * This method is called just before `node` is detached from the DOM. The return value of this
884 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
885 * is inserted into DOM to replace `node`.
888 * @param {HTMLElement} node
889 * @param {Object} config
892 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
897 * Get a jQuery function within a specific document.
900 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
901 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
903 * @return {Function} Bound jQuery function
905 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
906 function wrapper( selector
) {
907 return $( selector
, wrapper
.context
);
910 wrapper
.context
= this.getDocument( context
);
913 wrapper
.$iframe
= $iframe
;
920 * Get the document of an element.
923 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
924 * @return {HTMLDocument|null} Document object
926 OO
.ui
.Element
.static.getDocument = function ( obj
) {
927 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
928 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
929 // Empty jQuery selections might have a context
936 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
941 * Get the window of an element or document.
944 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
945 * @return {Window} Window object
947 OO
.ui
.Element
.static.getWindow = function ( obj
) {
948 var doc
= this.getDocument( obj
);
949 return doc
.defaultView
;
953 * Get the direction of an element or document.
956 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
957 * @return {string} Text direction, either 'ltr' or 'rtl'
959 OO
.ui
.Element
.static.getDir = function ( obj
) {
962 if ( obj
instanceof $ ) {
965 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
966 isWin
= obj
.document
!== undefined;
967 if ( isDoc
|| isWin
) {
973 return $( obj
).css( 'direction' );
977 * Get the offset between two frames.
979 * TODO: Make this function not use recursion.
982 * @param {Window} from Window of the child frame
983 * @param {Window} [to=window] Window of the parent frame
984 * @param {Object} [offset] Offset to start with, used internally
985 * @return {Object} Offset object, containing left and top properties
987 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
988 var i
, len
, frames
, frame
, rect
;
994 offset
= { top
: 0, left
: 0 };
996 if ( from.parent
=== from ) {
1000 // Get iframe element
1001 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
1002 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
1003 if ( frames
[ i
].contentWindow
=== from ) {
1004 frame
= frames
[ i
];
1009 // Recursively accumulate offset values
1011 rect
= frame
.getBoundingClientRect();
1012 offset
.left
+= rect
.left
;
1013 offset
.top
+= rect
.top
;
1014 if ( from !== to
) {
1015 this.getFrameOffset( from.parent
, offset
);
1022 * Get the offset between two elements.
1024 * The two elements may be in a different frame, but in that case the frame $element is in must
1025 * be contained in the frame $anchor is in.
1028 * @param {jQuery} $element Element whose position to get
1029 * @param {jQuery} $anchor Element to get $element's position relative to
1030 * @return {Object} Translated position coordinates, containing top and left properties
1032 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1033 var iframe
, iframePos
,
1034 pos
= $element
.offset(),
1035 anchorPos
= $anchor
.offset(),
1036 elementDocument
= this.getDocument( $element
),
1037 anchorDocument
= this.getDocument( $anchor
);
1039 // If $element isn't in the same document as $anchor, traverse up
1040 while ( elementDocument
!== anchorDocument
) {
1041 iframe
= elementDocument
.defaultView
.frameElement
;
1043 throw new Error( '$element frame is not contained in $anchor frame' );
1045 iframePos
= $( iframe
).offset();
1046 pos
.left
+= iframePos
.left
;
1047 pos
.top
+= iframePos
.top
;
1048 elementDocument
= iframe
.ownerDocument
;
1050 pos
.left
-= anchorPos
.left
;
1051 pos
.top
-= anchorPos
.top
;
1056 * Get element border sizes.
1059 * @param {HTMLElement} el Element to measure
1060 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1062 OO
.ui
.Element
.static.getBorders = function ( el
) {
1063 var doc
= el
.ownerDocument
,
1064 win
= doc
.defaultView
,
1065 style
= win
.getComputedStyle( el
, null ),
1067 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1068 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1069 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1070 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1081 * Get dimensions of an element or window.
1084 * @param {HTMLElement|Window} el Element to measure
1085 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1087 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1089 doc
= el
.ownerDocument
|| el
.document
,
1090 win
= doc
.defaultView
;
1092 if ( win
=== el
|| el
=== doc
.documentElement
) {
1095 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1097 top
: $win
.scrollTop(),
1098 left
: $win
.scrollLeft()
1100 scrollbar
: { right
: 0, bottom
: 0 },
1104 bottom
: $win
.innerHeight(),
1105 right
: $win
.innerWidth()
1111 borders
: this.getBorders( el
),
1113 top
: $el
.scrollTop(),
1114 left
: $el
.scrollLeft()
1117 right
: $el
.innerWidth() - el
.clientWidth
,
1118 bottom
: $el
.innerHeight() - el
.clientHeight
1120 rect
: el
.getBoundingClientRect()
1126 * Get the number of pixels that an element's content is scrolled to the left.
1128 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1129 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1131 * This function smooths out browser inconsistencies (nicely described in the README at
1132 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1133 * with Firefox's 'scrollLeft', which seems the sanest.
1137 * @param {HTMLElement|Window} el Element to measure
1138 * @return {number} Scroll position from the left.
1139 * If the element's direction is LTR, this is a positive number between `0` (initial scroll
1140 * position) and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1141 * If the element's direction is RTL, this is a negative number between `0` (initial scroll
1142 * position) and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1144 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1145 var rtlScrollType
= null;
1148 var $definer
= $( '<div>' ).attr( {
1150 style
: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
1152 definer
= $definer
[ 0 ];
1154 $definer
.appendTo( 'body' );
1155 if ( definer
.scrollLeft
> 0 ) {
1157 rtlScrollType
= 'default';
1159 definer
.scrollLeft
= 1;
1160 if ( definer
.scrollLeft
=== 0 ) {
1161 // Firefox, old Opera
1162 rtlScrollType
= 'negative';
1164 // Internet Explorer, Edge
1165 rtlScrollType
= 'reverse';
1171 return function getScrollLeft( el
) {
1172 var isRoot
= el
.window
=== el
||
1173 el
=== el
.ownerDocument
.body
||
1174 el
=== el
.ownerDocument
.documentElement
,
1175 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1176 // All browsers use the correct scroll type ('negative') on the root, so don't
1177 // do any fixups when looking at the root element
1178 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1180 if ( direction
=== 'rtl' ) {
1181 if ( rtlScrollType
=== null ) {
1184 if ( rtlScrollType
=== 'reverse' ) {
1185 scrollLeft
= -scrollLeft
;
1186 } else if ( rtlScrollType
=== 'default' ) {
1187 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1196 * Get the root scrollable element of given element's document.
1198 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1199 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1200 * lets us use 'body' or 'documentElement' based on what is working.
1202 * https://code.google.com/p/chromium/issues/detail?id=303131
1205 * @param {HTMLElement} el Element to find root scrollable parent for
1206 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1207 * depending on browser
1209 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1210 var scrollTop
, body
;
1212 if ( OO
.ui
.scrollableElement
=== undefined ) {
1213 body
= el
.ownerDocument
.body
;
1214 scrollTop
= body
.scrollTop
;
1217 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1218 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1219 if ( Math
.round( body
.scrollTop
) === 1 ) {
1220 body
.scrollTop
= scrollTop
;
1221 OO
.ui
.scrollableElement
= 'body';
1223 OO
.ui
.scrollableElement
= 'documentElement';
1227 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1231 * Get closest scrollable container.
1233 * Traverses up until either a scrollable element or the root is reached, in which case the root
1234 * scrollable element will be returned (see #getRootScrollableElement).
1237 * @param {HTMLElement} el Element to find scrollable container for
1238 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1239 * @return {HTMLElement} Closest scrollable container
1241 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1243 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1244 // 'overflow-y' have different values, so we need to check the separate properties.
1245 props
= [ 'overflow-x', 'overflow-y' ],
1246 $parent
= $( el
).parent();
1248 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1249 props
= [ 'overflow-' + dimension
];
1252 // Special case for the document root (which doesn't really have any scrollable container,
1253 // since it is the ultimate scrollable container, but this is probably saner than null or
1255 if ( $( el
).is( 'html, body' ) ) {
1256 return this.getRootScrollableElement( el
);
1259 while ( $parent
.length
) {
1260 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1261 return $parent
[ 0 ];
1265 val
= $parent
.css( props
[ i
] );
1266 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will
1267 // never be scrolled in that direction, but they can actually be scrolled
1268 // programatically. The user can unintentionally perform a scroll in such case even if
1269 // the application doesn't scroll programatically, e.g. when jumping to an anchor, or
1270 // when using built-in find functionality.
1271 // This could cause funny issues...
1272 if ( val
=== 'auto' || val
=== 'scroll' ) {
1273 return $parent
[ 0 ];
1276 $parent
= $parent
.parent();
1278 // The element is unattached... return something mostly sane
1279 return this.getRootScrollableElement( el
);
1283 * Scroll element into view.
1286 * @param {HTMLElement} el Element to scroll into view
1287 * @param {Object} [config] Configuration options
1288 * @param {string} [config.duration='fast'] jQuery animation duration value
1289 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1290 * to scroll in both directions
1291 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1293 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1294 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
,
1296 deferred
= $.Deferred();
1298 // Configuration initialization
1299 config
= config
|| {};
1302 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1303 $container
= $( container
);
1304 elementDimensions
= this.getDimensions( el
);
1305 containerDimensions
= this.getDimensions( container
);
1306 $window
= $( this.getWindow( el
) );
1308 // Compute the element's position relative to the container
1309 if ( $container
.is( 'html, body' ) ) {
1310 // If the scrollable container is the root, this is easy
1312 top
: elementDimensions
.rect
.top
,
1313 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1314 left
: elementDimensions
.rect
.left
,
1315 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1318 // Otherwise, we have to subtract el's coordinates from container's coordinates
1320 top
: elementDimensions
.rect
.top
-
1321 ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1322 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
-
1323 containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1324 left
: elementDimensions
.rect
.left
-
1325 ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1326 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
-
1327 containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1331 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1332 if ( position
.top
< 0 ) {
1333 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1334 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1335 animations
.scrollTop
= containerDimensions
.scroll
.top
+
1336 Math
.min( position
.top
, -position
.bottom
);
1339 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1340 if ( position
.left
< 0 ) {
1341 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1342 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1343 animations
.scrollLeft
= containerDimensions
.scroll
.left
+
1344 Math
.min( position
.left
, -position
.right
);
1347 if ( !$.isEmptyObject( animations
) ) {
1348 // eslint-disable-next-line no-jquery/no-animate
1349 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ?
1350 'fast' : config
.duration
);
1351 $container
.queue( function ( next
) {
1358 return deferred
.promise();
1362 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1363 * and reserve space for them, because it probably doesn't.
1365 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1366 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1367 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a
1368 * reflow, and then reattach (or show) them back.
1371 * @param {HTMLElement} el Element to reconsider the scrollbars on
1373 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1374 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1375 // Save scroll position
1376 scrollLeft
= el
.scrollLeft
;
1377 scrollTop
= el
.scrollTop
;
1378 // Detach all children
1379 while ( el
.firstChild
) {
1380 nodes
.push( el
.firstChild
);
1381 el
.removeChild( el
.firstChild
);
1384 // eslint-disable-next-line no-void
1385 void el
.offsetHeight
;
1386 // Reattach all children
1387 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1388 el
.appendChild( nodes
[ i
] );
1390 // Restore scroll position (no-op if scrollbars disappeared)
1391 el
.scrollLeft
= scrollLeft
;
1392 el
.scrollTop
= scrollTop
;
1398 * Toggle visibility of an element.
1400 * @param {boolean} [show] Make element visible, omit to toggle visibility
1403 * @return {OO.ui.Element} The element, for chaining
1405 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1406 show
= show
=== undefined ? !this.visible
: !!show
;
1408 if ( show
!== this.isVisible() ) {
1409 this.visible
= show
;
1410 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1411 this.emit( 'toggle', show
);
1418 * Check if element is visible.
1420 * @return {boolean} element is visible
1422 OO
.ui
.Element
.prototype.isVisible = function () {
1423 return this.visible
;
1429 * @return {Mixed} Element data
1431 OO
.ui
.Element
.prototype.getData = function () {
1438 * @param {Mixed} data Element data
1440 * @return {OO.ui.Element} The element, for chaining
1442 OO
.ui
.Element
.prototype.setData = function ( data
) {
1448 * Set the element has an 'id' attribute.
1450 * @param {string} id
1452 * @return {OO.ui.Element} The element, for chaining
1454 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1455 this.elementId
= id
;
1456 this.$element
.attr( 'id', id
);
1461 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1462 * and return its value.
1466 OO
.ui
.Element
.prototype.getElementId = function () {
1467 if ( this.elementId
=== null ) {
1468 this.setElementId( OO
.ui
.generateElementId() );
1470 return this.elementId
;
1474 * Check if element supports one or more methods.
1476 * @param {string|string[]} methods Method or list of methods to check
1477 * @return {boolean} All methods are supported
1479 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1483 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1484 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1485 if ( typeof this[ methods
[ i
] ] === 'function' ) {
1490 return methods
.length
=== support
;
1494 * Update the theme-provided classes.
1496 * @localdoc This is called in element mixins and widget classes any time state changes.
1497 * Updating is debounced, minimizing overhead of changing multiple attributes and
1498 * guaranteeing that theme updates do not occur within an element's constructor
1500 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1501 OO
.ui
.theme
.queueUpdateElementClasses( this );
1505 * Get the HTML tag name.
1507 * Override this method to base the result on instance information.
1509 * @return {string} HTML tag name
1511 OO
.ui
.Element
.prototype.getTagName = function () {
1512 return this.constructor.static.tagName
;
1516 * Check if the element is attached to the DOM
1518 * @return {boolean} The element is attached to the DOM
1520 OO
.ui
.Element
.prototype.isElementAttached = function () {
1521 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1525 * Get the DOM document.
1527 * @return {HTMLDocument} Document object
1529 OO
.ui
.Element
.prototype.getElementDocument = function () {
1530 // Don't cache this in other ways either because subclasses could can change this.$element
1531 return OO
.ui
.Element
.static.getDocument( this.$element
);
1535 * Get the DOM window.
1537 * @return {Window} Window object
1539 OO
.ui
.Element
.prototype.getElementWindow = function () {
1540 return OO
.ui
.Element
.static.getWindow( this.$element
);
1544 * Get closest scrollable container.
1546 * @return {HTMLElement} Closest scrollable container
1548 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1549 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1553 * Get group element is in.
1555 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1557 OO
.ui
.Element
.prototype.getElementGroup = function () {
1558 return this.elementGroup
;
1562 * Set group element is in.
1564 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1566 * @return {OO.ui.Element} The element, for chaining
1568 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1569 this.elementGroup
= group
;
1574 * Scroll element into view.
1576 * @param {Object} [config] Configuration options
1577 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1579 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1581 !this.isElementAttached() ||
1582 !this.isVisible() ||
1583 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1585 return $.Deferred().resolve();
1587 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1591 * Restore the pre-infusion dynamic state for this widget.
1593 * This method is called after #$element has been inserted into DOM. The parameter is the return
1594 * value of #gatherPreInfuseState.
1597 * @param {Object} state
1599 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1603 * Wraps an HTML snippet for use with configuration values which default
1604 * to strings. This bypasses the default html-escaping done to string
1610 * @param {string} [content] HTML content
1612 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1614 this.content
= content
;
1619 OO
.initClass( OO
.ui
.HtmlSnippet
);
1626 * @return {string} Unchanged HTML snippet.
1628 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1629 return this.content
;
1633 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in
1634 * a way that is centrally controlled and can be updated dynamically. Layouts can be, and usually
1636 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout},
1637 * {@link OO.ui.FormLayout FormLayout}, {@link OO.ui.PanelLayout PanelLayout},
1638 * {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1639 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout}
1640 * for more information and examples.
1644 * @extends OO.ui.Element
1645 * @mixins OO.EventEmitter
1648 * @param {Object} [config] Configuration options
1650 OO
.ui
.Layout
= function OoUiLayout( config
) {
1651 // Configuration initialization
1652 config
= config
|| {};
1654 // Parent constructor
1655 OO
.ui
.Layout
.parent
.call( this, config
);
1657 // Mixin constructors
1658 OO
.EventEmitter
.call( this );
1661 this.$element
.addClass( 'oo-ui-layout' );
1666 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1667 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1672 * Reset scroll offsets
1675 * @return {OO.ui.Layout} The layout, for chaining
1677 OO
.ui
.Layout
.prototype.resetScroll = function () {
1678 this.$element
[ 0 ].scrollTop
= 0;
1679 // TODO: Reset scrollLeft in an RTL-aware manner, see OO.ui.Element.static.getScrollLeft.
1685 * Widgets are compositions of one or more OOUI elements that users can both view
1686 * and interact with. All widgets can be configured and modified via a standard API,
1687 * and their state can change dynamically according to a model.
1691 * @extends OO.ui.Element
1692 * @mixins OO.EventEmitter
1695 * @param {Object} [config] Configuration options
1696 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1697 * appearance reflects this state.
1699 OO
.ui
.Widget
= function OoUiWidget( config
) {
1700 // Initialize config
1701 config
= $.extend( { disabled
: false }, config
);
1703 // Parent constructor
1704 OO
.ui
.Widget
.parent
.call( this, config
);
1706 // Mixin constructors
1707 OO
.EventEmitter
.call( this );
1710 this.disabled
= null;
1711 this.wasDisabled
= null;
1714 this.$element
.addClass( 'oo-ui-widget' );
1715 this.setDisabled( !!config
.disabled
);
1720 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1721 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1728 * A 'disable' event is emitted when the disabled state of the widget changes
1729 * (i.e. on disable **and** enable).
1731 * @param {boolean} disabled Widget is disabled
1737 * A 'toggle' event is emitted when the visibility of the widget changes.
1739 * @param {boolean} visible Widget is visible
1745 * Check if the widget is disabled.
1747 * @return {boolean} Widget is disabled
1749 OO
.ui
.Widget
.prototype.isDisabled = function () {
1750 return this.disabled
;
1754 * Set the 'disabled' state of the widget.
1756 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1758 * @param {boolean} disabled Disable widget
1760 * @return {OO.ui.Widget} The widget, for chaining
1762 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1765 this.disabled
= !!disabled
;
1766 isDisabled
= this.isDisabled();
1767 if ( isDisabled
!== this.wasDisabled
) {
1768 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1769 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1770 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1771 this.emit( 'disable', isDisabled
);
1772 this.updateThemeClasses();
1774 this.wasDisabled
= isDisabled
;
1780 * Update the disabled state, in case of changes in parent widget.
1783 * @return {OO.ui.Widget} The widget, for chaining
1785 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1786 this.setDisabled( this.disabled
);
1791 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1794 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1797 * @return {string|null} The ID of the labelable element
1799 OO
.ui
.Widget
.prototype.getInputId = function () {
1804 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1805 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1806 * override this method to provide intuitive, accessible behavior.
1808 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1809 * Individual widgets may override it too.
1811 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1814 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1825 OO
.ui
.Theme
= function OoUiTheme() {
1826 this.elementClassesQueue
= [];
1827 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1832 OO
.initClass( OO
.ui
.Theme
);
1837 * Get a list of classes to be applied to a widget.
1839 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1840 * otherwise state transitions will not work properly.
1842 * @param {OO.ui.Element} element Element for which to get classes
1843 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1845 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1846 return { on
: [], off
: [] };
1850 * Update CSS classes provided by the theme.
1852 * For elements with theme logic hooks, this should be called any time there's a state change.
1854 * @param {OO.ui.Element} element Element for which to update classes
1856 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1857 var $elements
= $( [] ),
1858 classes
= this.getElementClasses( element
);
1860 if ( element
.$icon
) {
1861 $elements
= $elements
.add( element
.$icon
);
1863 if ( element
.$indicator
) {
1864 $elements
= $elements
.add( element
.$indicator
);
1868 .removeClass( classes
.off
)
1869 .addClass( classes
.on
);
1875 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1877 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1878 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1881 this.elementClassesQueue
= [];
1885 * Queue #updateElementClasses to be called for this element.
1887 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1888 * to make them synchronous.
1890 * @param {OO.ui.Element} element Element for which to update classes
1892 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1893 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1894 // the most common case (this method is often called repeatedly for the same element).
1895 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1898 this.elementClassesQueue
.push( element
);
1899 this.debouncedUpdateQueuedElementClasses();
1903 * Get the transition duration in milliseconds for dialogs opening/closing
1905 * The dialog should be fully rendered this many milliseconds after the
1906 * ready process has executed.
1908 * @return {number} Transition duration in milliseconds
1910 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1915 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1916 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1917 * order in which users will navigate through the focusable elements via the Tab key.
1920 * // TabIndexedElement is mixed into the ButtonWidget class
1921 * // to provide a tabIndex property.
1922 * var button1 = new OO.ui.ButtonWidget( {
1926 * button2 = new OO.ui.ButtonWidget( {
1930 * button3 = new OO.ui.ButtonWidget( {
1934 * button4 = new OO.ui.ButtonWidget( {
1938 * $( document.body ).append(
1949 * @param {Object} [config] Configuration options
1950 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1951 * the functionality is applied to the element created by the class ($element). If a different
1952 * element is specified, the tabindex functionality will be applied to it instead.
1953 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the
1954 * tab-navigation order (e.g., 1 for the first focusable element). Use 0 to use the default
1955 * navigation order; use -1 to remove the element from the tab-navigation flow.
1957 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1958 // Configuration initialization
1959 config
= $.extend( { tabIndex
: 0 }, config
);
1962 this.$tabIndexed
= null;
1963 this.tabIndex
= null;
1966 this.connect( this, {
1967 disable
: 'onTabIndexedElementDisable'
1971 this.setTabIndex( config
.tabIndex
);
1972 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1977 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1982 * Set the element that should use the tabindex functionality.
1984 * This method is used to retarget a tabindex mixin so that its functionality applies
1985 * to the specified element. If an element is currently using the functionality, the mixin’s
1986 * effect on that element is removed before the new element is set up.
1988 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1990 * @return {OO.ui.Element} The element, for chaining
1992 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1993 var tabIndex
= this.tabIndex
;
1994 // Remove attributes from old $tabIndexed
1995 this.setTabIndex( null );
1996 // Force update of new $tabIndexed
1997 this.$tabIndexed
= $tabIndexed
;
1998 this.tabIndex
= tabIndex
;
1999 return this.updateTabIndex();
2003 * Set the value of the tabindex.
2005 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
2007 * @return {OO.ui.Element} The element, for chaining
2009 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
2010 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
2012 if ( this.tabIndex
!== tabIndex
) {
2013 this.tabIndex
= tabIndex
;
2014 this.updateTabIndex();
2021 * Update the `tabindex` attribute, in case of changes to tab index or
2026 * @return {OO.ui.Element} The element, for chaining
2028 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
2029 if ( this.$tabIndexed
) {
2030 if ( this.tabIndex
!== null ) {
2031 // Do not index over disabled elements
2032 this.$tabIndexed
.attr( {
2033 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
2034 // Support: ChromeVox and NVDA
2035 // These do not seem to inherit aria-disabled from parent elements
2036 'aria-disabled': this.isDisabled().toString()
2039 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
2046 * Handle disable events.
2049 * @param {boolean} disabled Element is disabled
2051 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
2052 this.updateTabIndex();
2056 * Get the value of the tabindex.
2058 * @return {number|null} Tabindex value
2060 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2061 return this.tabIndex
;
2065 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2067 * If the element already has an ID then that is returned, otherwise unique ID is
2068 * generated, set on the element, and returned.
2070 * @return {string|null} The ID of the focusable element
2072 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2075 if ( !this.$tabIndexed
) {
2078 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2082 id
= this.$tabIndexed
.attr( 'id' );
2083 if ( id
=== undefined ) {
2084 id
= OO
.ui
.generateElementId();
2085 this.$tabIndexed
.attr( 'id', id
);
2092 * Whether the node is 'labelable' according to the HTML spec
2093 * (i.e., whether it can be interacted with through a `<label for="…">`).
2094 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2097 * @param {jQuery} $node
2100 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2102 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2103 tagName
= ( $node
.prop( 'tagName' ) || '' ).toLowerCase();
2105 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2108 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2115 * Focus this element.
2118 * @return {OO.ui.Element} The element, for chaining
2120 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2121 if ( !this.isDisabled() ) {
2122 this.$tabIndexed
.trigger( 'focus' );
2128 * Blur this element.
2131 * @return {OO.ui.Element} The element, for chaining
2133 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2134 this.$tabIndexed
.trigger( 'blur' );
2139 * @inheritdoc OO.ui.Widget
2141 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2146 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2147 * interface element that can be configured with access keys for keyboard interaction.
2148 * See the [OOUI documentation on MediaWiki] [1] for examples.
2150 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2156 * @param {Object} [config] Configuration options
2157 * @cfg {jQuery} [$button] The button element created by the class.
2158 * If this configuration is omitted, the button element will use a generated `<a>`.
2159 * @cfg {boolean} [framed=true] Render the button with a frame
2161 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2162 // Configuration initialization
2163 config
= config
|| {};
2166 this.$button
= null;
2168 this.active
= config
.active
!== undefined && config
.active
;
2169 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
2170 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2171 this.onDocumentKeyUpHandler
= this.onDocumentKeyUp
.bind( this );
2172 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2173 this.onClickHandler
= this.onClick
.bind( this );
2174 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2177 this.$element
.addClass( 'oo-ui-buttonElement' );
2178 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2179 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2184 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2186 /* Static Properties */
2189 * Cancel mouse down events.
2191 * This property is usually set to `true` to prevent the focus from changing when the button is
2193 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and
2194 * {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} use a value of `false` so that dragging
2195 * behavior is possible and mousedown events can be handled by a parent widget.
2199 * @property {boolean}
2201 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2206 * A 'click' event is emitted when the button element is clicked.
2214 * Set the button element.
2216 * This method is used to retarget a button mixin so that its functionality applies to
2217 * the specified button element instead of the one created by the class. If a button element
2218 * is already set, the method will remove the mixin’s effect on that element.
2220 * @param {jQuery} $button Element to use as button
2222 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2223 if ( this.$button
) {
2225 .removeClass( 'oo-ui-buttonElement-button' )
2226 .removeAttr( 'role accesskey' )
2228 mousedown
: this.onMouseDownHandler
,
2229 keydown
: this.onKeyDownHandler
,
2230 click
: this.onClickHandler
,
2231 keypress
: this.onKeyPressHandler
2235 this.$button
= $button
2236 .addClass( 'oo-ui-buttonElement-button' )
2238 mousedown
: this.onMouseDownHandler
,
2239 keydown
: this.onKeyDownHandler
,
2240 click
: this.onClickHandler
,
2241 keypress
: this.onKeyPressHandler
2244 // Add `role="button"` on `<a>` elements, where it's needed
2245 // `toUpperCase()` is added for XHTML documents
2246 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2247 this.$button
.attr( 'role', 'button' );
2252 * Handles mouse down events.
2255 * @param {jQuery.Event} e Mouse down event
2256 * @return {undefined/boolean} False to prevent default if event is handled
2258 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2259 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2262 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2263 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2264 // reliably remove the pressed class
2265 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2266 // Prevent change of focus unless specifically configured otherwise
2267 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2273 * Handles document mouse up events.
2276 * @param {MouseEvent} e Mouse up event
2278 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentMouseUp = function ( e
) {
2279 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2282 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2283 // Stop listening for mouseup, since we only needed this once
2284 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2288 * Handles mouse click events.
2291 * @param {jQuery.Event} e Mouse click event
2293 * @return {undefined/boolean} False to prevent default if event is handled
2295 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2296 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2297 if ( this.emit( 'click' ) ) {
2304 * Handles key down events.
2307 * @param {jQuery.Event} e Key down event
2309 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2310 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2313 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2314 // Run the keyup handler no matter where the key is when the button is let go, so we can
2315 // reliably remove the pressed class
2316 this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2320 * Handles document key up events.
2323 * @param {KeyboardEvent} e Key up event
2325 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentKeyUp = function ( e
) {
2326 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2329 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2330 // Stop listening for keyup, since we only needed this once
2331 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2335 * Handles key press events.
2338 * @param {jQuery.Event} e Key press event
2340 * @return {undefined/boolean} False to prevent default if event is handled
2342 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2343 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2344 if ( this.emit( 'click' ) ) {
2351 * Check if button has a frame.
2353 * @return {boolean} Button is framed
2355 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2360 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame
2363 * @param {boolean} [framed] Make button framed, omit to toggle
2365 * @return {OO.ui.Element} The element, for chaining
2367 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2368 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2369 if ( framed
!== this.framed
) {
2370 this.framed
= framed
;
2372 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2373 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2374 this.updateThemeClasses();
2381 * Set the button's active state.
2383 * The active state can be set on:
2385 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2386 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2387 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2390 * @param {boolean} value Make button active
2392 * @return {OO.ui.Element} The element, for chaining
2394 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2395 this.active
= !!value
;
2396 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2397 this.updateThemeClasses();
2402 * Check if the button is active
2405 * @return {boolean} The button is active
2407 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2412 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2413 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2414 * items from the group is done through the interface the class provides.
2415 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2417 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2420 * @mixins OO.EmitterList
2424 * @param {Object} [config] Configuration options
2425 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2426 * is omitted, the group element will use a generated `<div>`.
2428 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2429 // Configuration initialization
2430 config
= config
|| {};
2432 // Mixin constructors
2433 OO
.EmitterList
.call( this, config
);
2439 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2444 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2451 * A change event is emitted when the set of selected items changes.
2453 * @param {OO.ui.Element[]} items Items currently in the group
2459 * Set the group element.
2461 * If an element is already set, items will be moved to the new element.
2463 * @param {jQuery} $group Element to use as group
2465 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2468 this.$group
= $group
;
2469 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2470 this.$group
.append( this.items
[ i
].$element
);
2475 * Find an item by its data.
2477 * Only the first item with matching data will be returned. To return all matching items,
2478 * use the #findItemsFromData method.
2480 * @param {Object} data Item data to search for
2481 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2483 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2485 hash
= OO
.getHash( data
);
2487 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2488 item
= this.items
[ i
];
2489 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2498 * Find items by their data.
2500 * All items with matching data will be returned. To return only the first match, use the
2501 * #findItemFromData method instead.
2503 * @param {Object} data Item data to search for
2504 * @return {OO.ui.Element[]} Items with equivalent data
2506 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2508 hash
= OO
.getHash( data
),
2511 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2512 item
= this.items
[ i
];
2513 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2522 * Add items to the group.
2524 * Items will be added to the end of the group array unless the optional `index` parameter
2525 * specifies a different insertion point. Adding an existing item will move it to the end of the
2526 * array or the point specified by the `index`.
2528 * @param {OO.ui.Element[]} items An array of items to add to the group
2529 * @param {number} [index] Index of the insertion point
2531 * @return {OO.ui.Element} The element, for chaining
2533 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2535 if ( items
.length
=== 0 ) {
2540 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2542 this.emit( 'change', this.getItems() );
2549 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2550 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2551 this.insertItemElements( items
, newIndex
);
2554 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2562 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2563 item
.setElementGroup( this );
2564 this.insertItemElements( item
, index
);
2567 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2573 * Insert elements into the group
2576 * @param {OO.ui.Element} itemWidget Item to insert
2577 * @param {number} index Insertion index
2579 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2580 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2581 this.$group
.append( itemWidget
.$element
);
2582 } else if ( index
=== 0 ) {
2583 this.$group
.prepend( itemWidget
.$element
);
2585 this.items
[ index
].$element
.before( itemWidget
.$element
);
2590 * Remove the specified items from a group.
2592 * Removed items are detached (not removed) from the DOM so that they may be reused.
2593 * To remove all items from a group, you may wish to use the #clearItems method instead.
2595 * @param {OO.ui.Element[]} items An array of items to remove
2597 * @return {OO.ui.Element} The element, for chaining
2599 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2600 var i
, len
, item
, index
;
2602 if ( items
.length
=== 0 ) {
2606 // Remove specific items elements
2607 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2609 index
= this.items
.indexOf( item
);
2610 if ( index
!== -1 ) {
2611 item
.setElementGroup( null );
2612 item
.$element
.detach();
2617 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2619 this.emit( 'change', this.getItems() );
2624 * Clear all items from the group.
2626 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2627 * To remove only a subset of items from a group, use the #removeItems method.
2630 * @return {OO.ui.Element} The element, for chaining
2632 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2635 // Remove all item elements
2636 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2637 this.items
[ i
].setElementGroup( null );
2638 this.items
[ i
].$element
.detach();
2642 OO
.EmitterList
.prototype.clearItems
.call( this );
2644 this.emit( 'change', this.getItems() );
2649 * LabelElement is often mixed into other classes to generate a label, which
2650 * helps identify the function of an interface element.
2651 * See the [OOUI documentation on MediaWiki] [1] for more information.
2653 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2659 * @param {Object} [config] Configuration options
2660 * @cfg {jQuery} [$label] The label element created by the class. If this
2661 * configuration is omitted, the label element will use a generated `<span>`.
2662 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be
2663 * specified as a plaintext string, a jQuery selection of elements, or a function that will
2664 * produce a string in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2665 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2666 * @cfg {boolean} [invisibleLabel] Whether the label should be visually hidden (but still
2667 * accessible to screen-readers).
2669 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2670 // Configuration initialization
2671 config
= config
|| {};
2676 this.invisibleLabel
= null;
2679 this.setLabel( config
.label
|| this.constructor.static.label
);
2680 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2681 this.setInvisibleLabel( config
.invisibleLabel
);
2686 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2691 * @event labelChange
2692 * @param {string} value
2695 /* Static Properties */
2698 * The label text. The label can be specified as a plaintext string, a function that will
2699 * produce a string in the future, or `null` for no label. The static value will
2700 * be overridden if a label is specified with the #label config option.
2704 * @property {string|Function|null}
2706 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2708 /* Static methods */
2711 * Highlight the first occurrence of the query in the given text
2713 * @param {string} text Text
2714 * @param {string} query Query to find
2715 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2716 * @return {jQuery} Text with the first match of the query
2717 * sub-string wrapped in highlighted span
2719 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2722 $result
= $( '<span>' );
2726 qLen
= query
.length
;
2727 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2728 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2733 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2736 if ( !query
.length
|| offset
=== -1 ) {
2737 $result
.text( text
);
2740 document
.createTextNode( text
.slice( 0, offset
) ),
2742 .addClass( 'oo-ui-labelElement-label-highlight' )
2743 .text( text
.slice( offset
, offset
+ query
.length
) ),
2744 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2747 return $result
.contents();
2753 * Set the label element.
2755 * If an element is already set, it will be cleaned up before setting up the new element.
2757 * @param {jQuery} $label Element to use as label
2759 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2760 if ( this.$label
) {
2761 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2764 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2765 this.setLabelContent( this.label
);
2771 * An empty string will result in the label being hidden. A string containing only whitespace will
2772 * be converted to a single ` `.
2774 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that
2775 * returns nodes or text; or null for no label
2777 * @return {OO.ui.Element} The element, for chaining
2779 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2780 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2781 label
= ( ( typeof label
=== 'string' || label
instanceof $ ) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2783 if ( this.label
!== label
) {
2784 if ( this.$label
) {
2785 this.setLabelContent( label
);
2788 this.emit( 'labelChange' );
2791 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2797 * Set whether the label should be visually hidden (but still accessible to screen-readers).
2799 * @param {boolean} invisibleLabel
2801 * @return {OO.ui.Element} The element, for chaining
2803 OO
.ui
.mixin
.LabelElement
.prototype.setInvisibleLabel = function ( invisibleLabel
) {
2804 invisibleLabel
= !!invisibleLabel
;
2806 if ( this.invisibleLabel
!== invisibleLabel
) {
2807 this.invisibleLabel
= invisibleLabel
;
2808 this.emit( 'labelChange' );
2811 this.$label
.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel
);
2812 // Pretend that there is no label, a lot of CSS has been written with this assumption
2813 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2819 * Set the label as plain text with a highlighted query
2821 * @param {string} text Text label to set
2822 * @param {string} query Substring of text to highlight
2823 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2825 * @return {OO.ui.Element} The element, for chaining
2827 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
2828 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
2834 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2835 * text; or null for no label
2837 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2842 * Set the content of the label.
2844 * Do not call this method until after the label element has been set by #setLabelElement.
2847 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2848 * text; or null for no label
2850 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2851 if ( typeof label
=== 'string' ) {
2852 if ( label
.match( /^\s*$/ ) ) {
2853 // Convert whitespace only string to a single non-breaking space
2854 this.$label
.html( ' ' );
2856 this.$label
.text( label
);
2858 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2859 this.$label
.html( label
.toString() );
2860 } else if ( label
instanceof $ ) {
2861 this.$label
.empty().append( label
);
2863 this.$label
.empty();
2868 * IconElement is often mixed into other classes to generate an icon.
2869 * Icons are graphics, about the size of normal text. They are used to aid the user
2870 * in locating a control or to convey information in a space-efficient way. See the
2871 * [OOUI documentation on MediaWiki] [1] for a list of icons
2872 * included in the library.
2874 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2880 * @param {Object} [config] Configuration options
2881 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2882 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2883 * the icon element be set to an existing icon instead of the one generated by this class, set a
2884 * value using a jQuery selection. For example:
2886 * // Use a <div> tag instead of a <span>
2887 * $icon: $( '<div>' )
2888 * // Use an existing icon element instead of the one generated by the class
2889 * $icon: this.$element
2890 * // Use an icon element from a child widget
2891 * $icon: this.childwidget.$element
2892 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a
2893 * map of symbolic names. A map is used for i18n purposes and contains a `default` icon
2894 * name and additional names keyed by language code. The `default` name is used when no icon is
2895 * keyed by the user's language.
2897 * Example of an i18n map:
2899 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2900 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2901 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2903 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2904 // Configuration initialization
2905 config
= config
|| {};
2912 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2913 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2918 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2920 /* Static Properties */
2923 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map
2924 * is used for i18n purposes and contains a `default` icon name and additional names keyed by
2925 * language code. The `default` name is used when no icon is keyed by the user's language.
2927 * Example of an i18n map:
2929 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2931 * Note: the static property will be overridden if the #icon configuration is used.
2935 * @property {Object|string}
2937 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2940 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2941 * function that returns title text, or `null` for no title.
2943 * The static property will be overridden if the #iconTitle configuration is used.
2947 * @property {string|Function|null}
2949 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2954 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2955 * applies to the specified icon element instead of the one created by the class. If an icon
2956 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2957 * and mixin methods will no longer affect the element.
2959 * @param {jQuery} $icon Element to use as icon
2961 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2964 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2965 .removeAttr( 'title' );
2969 .addClass( 'oo-ui-iconElement-icon' )
2970 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
2971 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2972 if ( this.iconTitle
!== null ) {
2973 this.$icon
.attr( 'title', this.iconTitle
);
2976 this.updateThemeClasses();
2980 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2981 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2984 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2985 * by language code, or `null` to remove the icon.
2987 * @return {OO.ui.Element} The element, for chaining
2989 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2990 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2991 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2993 if ( this.icon
!== icon
) {
2995 if ( this.icon
!== null ) {
2996 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2998 if ( icon
!== null ) {
2999 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
3005 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
3007 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
3009 this.updateThemeClasses();
3015 * Get the symbolic name of the icon.
3017 * @return {string} Icon name
3019 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
3024 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
3026 * @return {string} Icon title text
3029 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
3030 return this.iconTitle
;
3034 * IndicatorElement is often mixed into other classes to generate an indicator.
3035 * Indicators are small graphics that are generally used in two ways:
3037 * - To draw attention to the status of an item. For example, an indicator might be
3038 * used to show that an item in a list has errors that need to be resolved.
3039 * - To clarify the function of a control that acts in an exceptional way (a button
3040 * that opens a menu instead of performing an action directly, for example).
3042 * For a list of indicators included in the library, please see the
3043 * [OOUI documentation on MediaWiki] [1].
3045 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3051 * @param {Object} [config] Configuration options
3052 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
3053 * configuration is omitted, the indicator element will use a generated `<span>`.
3054 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3055 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3057 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3059 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
3060 // Configuration initialization
3061 config
= config
|| {};
3064 this.$indicator
= null;
3065 this.indicator
= null;
3068 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
3069 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
3074 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
3076 /* Static Properties */
3079 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3080 * The static property will be overridden if the #indicator configuration is used.
3084 * @property {string|null}
3086 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
3089 * A text string used as the indicator title, a function that returns title text, or `null`
3090 * for no title. The static property will be overridden if the #indicatorTitle configuration is
3095 * @property {string|Function|null}
3097 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
3102 * Set the indicator element.
3104 * If an element is already set, it will be cleaned up before setting up the new element.
3106 * @param {jQuery} $indicator Element to use as indicator
3108 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
3109 if ( this.$indicator
) {
3111 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
3112 .removeAttr( 'title' );
3115 this.$indicator
= $indicator
3116 .addClass( 'oo-ui-indicatorElement-indicator' )
3117 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
3118 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
3119 if ( this.indicatorTitle
!== null ) {
3120 this.$indicator
.attr( 'title', this.indicatorTitle
);
3123 this.updateThemeClasses();
3127 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null`
3128 * to remove the indicator.
3130 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3132 * @return {OO.ui.Element} The element, for chaining
3134 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
3135 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
3137 if ( this.indicator
!== indicator
) {
3138 if ( this.$indicator
) {
3139 if ( this.indicator
!== null ) {
3140 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
3142 if ( indicator
!== null ) {
3143 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
3146 this.indicator
= indicator
;
3149 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
3150 if ( this.$indicator
) {
3151 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
3153 this.updateThemeClasses();
3159 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3161 * @return {string} Symbolic name of indicator
3163 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
3164 return this.indicator
;
3168 * Get the indicator title.
3170 * The title is displayed when a user moves the mouse over the indicator.
3172 * @return {string} Indicator title text
3175 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
3176 return this.indicatorTitle
;
3180 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3181 * additional functionality to an element created by another class. The class provides
3182 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3183 * which are used to customize the look and feel of a widget to better describe its
3184 * importance and functionality.
3186 * The library currently contains the following styling flags for general use:
3188 * - **progressive**: Progressive styling is applied to convey that the widget will move the user
3189 * forward in a process.
3190 * - **destructive**: Destructive styling is applied to convey that the widget will remove
3193 * The flags affect the appearance of the buttons:
3196 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3197 * var button1 = new OO.ui.ButtonWidget( {
3198 * label: 'Progressive',
3199 * flags: 'progressive'
3201 * button2 = new OO.ui.ButtonWidget( {
3202 * label: 'Destructive',
3203 * flags: 'destructive'
3205 * $( document.body ).append( button1.$element, button2.$element );
3207 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an
3208 * action, use these flags: **primary** and **safe**.
3209 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3211 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3217 * @param {Object} [config] Configuration options
3218 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary')
3220 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3221 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3222 * @cfg {jQuery} [$flagged] The flagged element. By default,
3223 * the flagged functionality is applied to the element created by the class ($element).
3224 * If a different element is specified, the flagged functionality will be applied to it instead.
3226 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3227 // Configuration initialization
3228 config
= config
|| {};
3232 this.$flagged
= null;
3235 this.setFlags( config
.flags
);
3236 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3243 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3244 * parameter contains the name of each modified flag and indicates whether it was
3247 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3248 * that the flag was added, `false` that the flag was removed.
3254 * Set the flagged element.
3256 * This method is used to retarget a flagged mixin so that its functionality applies to the
3257 * specified element.
3258 * If an element is already set, the method will remove the mixin’s effect on that element.
3260 * @param {jQuery} $flagged Element that should be flagged
3262 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3263 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3264 return 'oo-ui-flaggedElement-' + flag
;
3267 if ( this.$flagged
) {
3268 this.$flagged
.removeClass( classNames
);
3271 this.$flagged
= $flagged
.addClass( classNames
);
3275 * Check if the specified flag is set.
3277 * @param {string} flag Name of flag
3278 * @return {boolean} The flag is set
3280 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3281 // This may be called before the constructor, thus before this.flags is set
3282 return this.flags
&& ( flag
in this.flags
);
3286 * Get the names of all flags set.
3288 * @return {string[]} Flag names
3290 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3291 // This may be called before the constructor, thus before this.flags is set
3292 return Object
.keys( this.flags
|| {} );
3299 * @return {OO.ui.Element} The element, for chaining
3302 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3303 var flag
, className
,
3306 classPrefix
= 'oo-ui-flaggedElement-';
3308 for ( flag
in this.flags
) {
3309 className
= classPrefix
+ flag
;
3310 changes
[ flag
] = false;
3311 delete this.flags
[ flag
];
3312 remove
.push( className
);
3315 if ( this.$flagged
) {
3316 this.$flagged
.removeClass( remove
);
3319 this.updateThemeClasses();
3320 this.emit( 'flag', changes
);
3326 * Add one or more flags.
3328 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3329 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3330 * be added (`true`) or removed (`false`).
3332 * @return {OO.ui.Element} The element, for chaining
3335 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3336 var i
, len
, flag
, className
,
3340 classPrefix
= 'oo-ui-flaggedElement-';
3342 if ( typeof flags
=== 'string' ) {
3343 className
= classPrefix
+ flags
;
3345 if ( !this.flags
[ flags
] ) {
3346 this.flags
[ flags
] = true;
3347 add
.push( className
);
3349 } else if ( Array
.isArray( flags
) ) {
3350 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3352 className
= classPrefix
+ flag
;
3354 if ( !this.flags
[ flag
] ) {
3355 changes
[ flag
] = true;
3356 this.flags
[ flag
] = true;
3357 add
.push( className
);
3360 } else if ( OO
.isPlainObject( flags
) ) {
3361 for ( flag
in flags
) {
3362 className
= classPrefix
+ flag
;
3363 if ( flags
[ flag
] ) {
3365 if ( !this.flags
[ flag
] ) {
3366 changes
[ flag
] = true;
3367 this.flags
[ flag
] = true;
3368 add
.push( className
);
3372 if ( this.flags
[ flag
] ) {
3373 changes
[ flag
] = false;
3374 delete this.flags
[ flag
];
3375 remove
.push( className
);
3381 if ( this.$flagged
) {
3384 .removeClass( remove
);
3387 this.updateThemeClasses();
3388 this.emit( 'flag', changes
);
3394 * TitledElement is mixed into other classes to provide a `title` attribute.
3395 * Titles are rendered by the browser and are made visible when the user moves
3396 * the mouse over the element. Titles are not visible on touch devices.
3399 * // TitledElement provides a `title` attribute to the
3400 * // ButtonWidget class.
3401 * var button = new OO.ui.ButtonWidget( {
3402 * label: 'Button with Title',
3403 * title: 'I am a button'
3405 * $( document.body ).append( button.$element );
3411 * @param {Object} [config] Configuration options
3412 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3413 * If this config is omitted, the title functionality is applied to $element, the
3414 * element created by the class.
3415 * @cfg {string|Function} [title] The title text or a function that returns text. If
3416 * this config is omitted, the value of the {@link #static-title static title} property is used.
3418 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3419 // Configuration initialization
3420 config
= config
|| {};
3423 this.$titled
= null;
3427 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3428 this.setTitledElement( config
.$titled
|| this.$element
);
3433 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3435 /* Static Properties */
3438 * The title text, a function that returns text, or `null` for no title. The value of the static
3439 * property is overridden if the #title config option is used.
3441 * If the element has a default title (e.g. `<input type=file>`), `null` will allow that title to be
3442 * shown. Use empty string to suppress it.
3446 * @property {string|Function|null}
3448 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3453 * Set the titled element.
3455 * This method is used to retarget a TitledElement mixin so that its functionality applies to the
3456 * specified element.
3457 * If an element is already set, the mixin’s effect on that element is removed before the new
3458 * element is set up.
3460 * @param {jQuery} $titled Element that should use the 'titled' functionality
3462 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3463 if ( this.$titled
) {
3464 this.$titled
.removeAttr( 'title' );
3467 this.$titled
= $titled
;
3474 * @param {string|Function|null} title Title text, a function that returns text, or `null`
3477 * @return {OO.ui.Element} The element, for chaining
3479 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3480 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3481 title
= typeof title
=== 'string' ? title
: null;
3483 if ( this.title
!== title
) {
3492 * Update the title attribute, in case of changes to title or accessKey.
3496 * @return {OO.ui.Element} The element, for chaining
3498 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3499 var title
= this.getTitle();
3500 if ( this.$titled
) {
3501 if ( title
!== null ) {
3502 // Only if this is an AccessKeyedElement
3503 if ( this.formatTitleWithAccessKey
) {
3504 title
= this.formatTitleWithAccessKey( title
);
3506 this.$titled
.attr( 'title', title
);
3508 this.$titled
.removeAttr( 'title' );
3517 * @return {string} Title string
3519 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3524 * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
3525 * Access keys allow an user to go to a specific element by using
3526 * a shortcut combination of a browser specific keys + the key
3530 * // AccessKeyedElement provides an `accesskey` attribute to the
3531 * // ButtonWidget class.
3532 * var button = new OO.ui.ButtonWidget( {
3533 * label: 'Button with access key',
3536 * $( document.body ).append( button.$element );
3542 * @param {Object} [config] Configuration options
3543 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3544 * If this config is omitted, the access key functionality is applied to $element, the
3545 * element created by the class.
3546 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3547 * this config is omitted, no access key will be added.
3549 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3550 // Configuration initialization
3551 config
= config
|| {};
3554 this.$accessKeyed
= null;
3555 this.accessKey
= null;
3558 this.setAccessKey( config
.accessKey
|| null );
3559 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3561 // If this is also a TitledElement and it initialized before we did, we may have
3562 // to update the title with the access key
3563 if ( this.updateTitle
) {
3570 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3572 /* Static Properties */
3575 * The access key, a function that returns a key, or `null` for no access key.
3579 * @property {string|Function|null}
3581 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3586 * Set the access keyed element.
3588 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to
3589 * the specified element.
3590 * If an element is already set, the mixin's effect on that element is removed before the new
3591 * element is set up.
3593 * @param {jQuery} $accessKeyed Element that should use the 'access keyed' functionality
3595 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3596 if ( this.$accessKeyed
) {
3597 this.$accessKeyed
.removeAttr( 'accesskey' );
3600 this.$accessKeyed
= $accessKeyed
;
3601 if ( this.accessKey
) {
3602 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3609 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no
3612 * @return {OO.ui.Element} The element, for chaining
3614 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3615 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3617 if ( this.accessKey
!== accessKey
) {
3618 if ( this.$accessKeyed
) {
3619 if ( accessKey
!== null ) {
3620 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3622 this.$accessKeyed
.removeAttr( 'accesskey' );
3625 this.accessKey
= accessKey
;
3627 // Only if this is a TitledElement
3628 if ( this.updateTitle
) {
3639 * @return {string} accessKey string
3641 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3642 return this.accessKey
;
3646 * Add information about the access key to the element's tooltip label.
3647 * (This is only public for hacky usage in FieldLayout.)
3649 * @param {string} title Tooltip label for `title` attribute
3652 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3655 if ( !this.$accessKeyed
) {
3656 // Not initialized yet; the constructor will call updateTitle() which will rerun this
3660 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the
3662 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3663 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3665 accessKey
= this.getAccessKey();
3668 title
+= ' [' + accessKey
+ ']';
3674 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3675 * feels, and functionality can be customized via the class’s configuration options
3676 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3679 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3682 * // A button widget.
3683 * var button = new OO.ui.ButtonWidget( {
3684 * label: 'Button with Icon',
3688 * $( document.body ).append( button.$element );
3690 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3693 * @extends OO.ui.Widget
3694 * @mixins OO.ui.mixin.ButtonElement
3695 * @mixins OO.ui.mixin.IconElement
3696 * @mixins OO.ui.mixin.IndicatorElement
3697 * @mixins OO.ui.mixin.LabelElement
3698 * @mixins OO.ui.mixin.TitledElement
3699 * @mixins OO.ui.mixin.FlaggedElement
3700 * @mixins OO.ui.mixin.TabIndexedElement
3701 * @mixins OO.ui.mixin.AccessKeyedElement
3704 * @param {Object} [config] Configuration options
3705 * @cfg {boolean} [active=false] Whether button should be shown as active
3706 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3707 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3708 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3710 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3711 // Configuration initialization
3712 config
= config
|| {};
3714 // Parent constructor
3715 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3717 // Mixin constructors
3718 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3719 OO
.ui
.mixin
.IconElement
.call( this, config
);
3720 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3721 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3722 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
3723 $titled
: this.$button
3725 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3726 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
3727 $tabIndexed
: this.$button
3729 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
3730 $accessKeyed
: this.$button
3736 this.noFollow
= false;
3739 this.connect( this, {
3740 disable
: 'onDisable'
3744 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3746 .addClass( 'oo-ui-buttonWidget' )
3747 .append( this.$button
);
3748 this.setActive( config
.active
);
3749 this.setHref( config
.href
);
3750 this.setTarget( config
.target
);
3751 this.setNoFollow( config
.noFollow
);
3756 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3757 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3758 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3759 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3760 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3761 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3762 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3763 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3764 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3766 /* Static Properties */
3772 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3778 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3783 * Get hyperlink location.
3785 * @return {string} Hyperlink location
3787 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3792 * Get hyperlink target.
3794 * @return {string} Hyperlink target
3796 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3801 * Get search engine traversal hint.
3803 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3805 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3806 return this.noFollow
;
3810 * Set hyperlink location.
3812 * @param {string|null} href Hyperlink location, null to remove
3814 * @return {OO.ui.Widget} The widget, for chaining
3816 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3817 href
= typeof href
=== 'string' ? href
: null;
3818 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3822 if ( href
!== this.href
) {
3831 * Update the `href` attribute, in case of changes to href or
3836 * @return {OO.ui.Widget} The widget, for chaining
3838 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3839 if ( this.href
!== null && !this.isDisabled() ) {
3840 this.$button
.attr( 'href', this.href
);
3842 this.$button
.removeAttr( 'href' );
3849 * Handle disable events.
3852 * @param {boolean} disabled Element is disabled
3854 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3859 * Set hyperlink target.
3861 * @param {string|null} target Hyperlink target, null to remove
3862 * @return {OO.ui.Widget} The widget, for chaining
3864 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3865 target
= typeof target
=== 'string' ? target
: null;
3867 if ( target
!== this.target
) {
3868 this.target
= target
;
3869 if ( target
!== null ) {
3870 this.$button
.attr( 'target', target
);
3872 this.$button
.removeAttr( 'target' );
3880 * Set search engine traversal hint.
3882 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3883 * @return {OO.ui.Widget} The widget, for chaining
3885 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3886 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3888 if ( noFollow
!== this.noFollow
) {
3889 this.noFollow
= noFollow
;
3891 this.$button
.attr( 'rel', 'nofollow' );
3893 this.$button
.removeAttr( 'rel' );
3900 // Override method visibility hints from ButtonElement
3911 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3912 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3913 * removed, and cleared from the group.
3916 * // A ButtonGroupWidget with two buttons.
3917 * var button1 = new OO.ui.PopupButtonWidget( {
3918 * label: 'Select a category',
3921 * $content: $( '<p>List of categories…</p>' ),
3926 * button2 = new OO.ui.ButtonWidget( {
3929 * buttonGroup = new OO.ui.ButtonGroupWidget( {
3930 * items: [ button1, button2 ]
3932 * $( document.body ).append( buttonGroup.$element );
3935 * @extends OO.ui.Widget
3936 * @mixins OO.ui.mixin.GroupElement
3937 * @mixins OO.ui.mixin.TitledElement
3940 * @param {Object} [config] Configuration options
3941 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3943 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3944 // Configuration initialization
3945 config
= config
|| {};
3947 // Parent constructor
3948 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3950 // Mixin constructors
3951 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {
3952 $group
: this.$element
3954 OO
.ui
.mixin
.TitledElement
.call( this, config
);
3957 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3958 if ( Array
.isArray( config
.items
) ) {
3959 this.addItems( config
.items
);
3965 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3966 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3967 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.TitledElement
);
3969 /* Static Properties */
3975 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
3983 * @return {OO.ui.Widget} The widget, for chaining
3985 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
3986 if ( !this.isDisabled() ) {
3987 if ( this.items
[ 0 ] ) {
3988 this.items
[ 0 ].focus();
3997 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
4002 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}.
4003 * In general, IconWidgets should be used with OO.ui.LabelWidget, which creates a label that
4004 * identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
4005 * for a list of icons included in the library.
4008 * // An IconWidget with a label via LabelWidget.
4009 * var myIcon = new OO.ui.IconWidget( {
4013 * // Create a label.
4014 * iconLabel = new OO.ui.LabelWidget( {
4017 * $( document.body ).append( myIcon.$element, iconLabel.$element );
4019 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4022 * @extends OO.ui.Widget
4023 * @mixins OO.ui.mixin.IconElement
4024 * @mixins OO.ui.mixin.TitledElement
4025 * @mixins OO.ui.mixin.LabelElement
4026 * @mixins OO.ui.mixin.FlaggedElement
4029 * @param {Object} [config] Configuration options
4031 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
4032 // Configuration initialization
4033 config
= config
|| {};
4035 // Parent constructor
4036 OO
.ui
.IconWidget
.parent
.call( this, config
);
4038 // Mixin constructors
4039 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {
4040 $icon
: this.$element
4042 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4043 $titled
: this.$element
4045 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4046 $label
: this.$element
,
4047 invisibleLabel
: true
4049 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {
4050 $flagged
: this.$element
4054 this.$element
.addClass( 'oo-ui-iconWidget' );
4055 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4056 // nested in other widgets, because this widget used to not mix in LabelElement.
4057 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4062 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
4063 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
4064 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
4065 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.LabelElement
);
4066 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
4068 /* Static Properties */
4074 OO
.ui
.IconWidget
.static.tagName
= 'span';
4077 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4078 * attention to the status of an item or to clarify the function within a control. For a list of
4079 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4082 * // An indicator widget.
4083 * var indicator1 = new OO.ui.IndicatorWidget( {
4084 * indicator: 'required'
4086 * // Create a fieldset layout to add a label.
4087 * fieldset = new OO.ui.FieldsetLayout();
4088 * fieldset.addItems( [
4089 * new OO.ui.FieldLayout( indicator1, {
4090 * label: 'A required indicator:'
4093 * $( document.body ).append( fieldset.$element );
4095 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4098 * @extends OO.ui.Widget
4099 * @mixins OO.ui.mixin.IndicatorElement
4100 * @mixins OO.ui.mixin.TitledElement
4101 * @mixins OO.ui.mixin.LabelElement
4104 * @param {Object} [config] Configuration options
4106 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4107 // Configuration initialization
4108 config
= config
|| {};
4110 // Parent constructor
4111 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4113 // Mixin constructors
4114 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {
4115 $indicator
: this.$element
4117 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4118 $titled
: this.$element
4120 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4121 $label
: this.$element
,
4122 invisibleLabel
: true
4126 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4127 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4128 // nested in other widgets, because this widget used to not mix in LabelElement.
4129 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4134 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4135 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4136 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4137 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.LabelElement
);
4139 /* Static Properties */
4145 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4148 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4149 * be configured with a `label` option that is set to a string, a label node, or a function:
4151 * - String: a plaintext string
4152 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4153 * label that includes a link or special styling, such as a gray color or additional
4154 * graphical elements.
4155 * - Function: a function that will produce a string in the future. Functions are used
4156 * in cases where the value of the label is not currently defined.
4158 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget},
4159 * which will come into focus when the label is clicked.
4162 * // Two LabelWidgets.
4163 * var label1 = new OO.ui.LabelWidget( {
4164 * label: 'plaintext label'
4166 * label2 = new OO.ui.LabelWidget( {
4167 * label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4169 * // Create a fieldset layout with fields for each example.
4170 * fieldset = new OO.ui.FieldsetLayout();
4171 * fieldset.addItems( [
4172 * new OO.ui.FieldLayout( label1 ),
4173 * new OO.ui.FieldLayout( label2 )
4175 * $( document.body ).append( fieldset.$element );
4178 * @extends OO.ui.Widget
4179 * @mixins OO.ui.mixin.LabelElement
4180 * @mixins OO.ui.mixin.TitledElement
4183 * @param {Object} [config] Configuration options
4184 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4185 * Clicking the label will focus the specified input field.
4187 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4188 // Configuration initialization
4189 config
= config
|| {};
4191 // Parent constructor
4192 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4194 // Mixin constructors
4195 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4196 $label
: this.$element
4198 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4201 this.input
= config
.input
;
4205 if ( this.input
.getInputId() ) {
4206 this.$element
.attr( 'for', this.input
.getInputId() );
4208 this.$label
.on( 'click', function () {
4209 this.input
.simulateLabelClick();
4213 this.$element
.addClass( 'oo-ui-labelWidget' );
4218 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4219 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4220 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4222 /* Static Properties */
4228 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4231 * PendingElement is a mixin that is used to create elements that notify users that something is
4232 * happening and that they should wait before proceeding. The pending state is visually represented
4233 * with a pending texture that appears in the head of a pending
4234 * {@link OO.ui.ProcessDialog process dialog} or in the input field of a
4235 * {@link OO.ui.TextInputWidget text input widget}.
4237 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked
4238 * as pending, but only when used in {@link OO.ui.MessageDialog message dialogs}. The behavior is
4239 * not currently supported for action widgets used in process dialogs.
4242 * function MessageDialog( config ) {
4243 * MessageDialog.parent.call( this, config );
4245 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4247 * MessageDialog.static.name = 'myMessageDialog';
4248 * MessageDialog.static.actions = [
4249 * { action: 'save', label: 'Done', flags: 'primary' },
4250 * { label: 'Cancel', flags: 'safe' }
4253 * MessageDialog.prototype.initialize = function () {
4254 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4255 * this.content = new OO.ui.PanelLayout( { padded: true } );
4256 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending ' +
4257 * 'state. Note that action widgets can be marked pending in message dialogs but not ' +
4258 * 'process dialogs.</p>' );
4259 * this.$body.append( this.content.$element );
4261 * MessageDialog.prototype.getBodyHeight = function () {
4264 * MessageDialog.prototype.getActionProcess = function ( action ) {
4265 * var dialog = this;
4266 * if ( action === 'save' ) {
4267 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4268 * return new OO.ui.Process()
4270 * .next( function () {
4271 * dialog.getActions().get({actions: 'save'})[0].popPending();
4274 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4277 * var windowManager = new OO.ui.WindowManager();
4278 * $( document.body ).append( windowManager.$element );
4280 * var dialog = new MessageDialog();
4281 * windowManager.addWindows( [ dialog ] );
4282 * windowManager.openWindow( dialog );
4288 * @param {Object} [config] Configuration options
4289 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4291 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4292 // Configuration initialization
4293 config
= config
|| {};
4297 this.$pending
= null;
4300 this.setPendingElement( config
.$pending
|| this.$element
);
4305 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4310 * Set the pending element (and clean up any existing one).
4312 * @param {jQuery} $pending The element to set to pending.
4314 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4315 if ( this.$pending
) {
4316 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4319 this.$pending
= $pending
;
4320 if ( this.pending
> 0 ) {
4321 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4326 * Check if an element is pending.
4328 * @return {boolean} Element is pending
4330 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4331 return !!this.pending
;
4335 * Increase the pending counter. The pending state will remain active until the counter is zero
4336 * (i.e., the number of calls to #pushPending and #popPending is the same).
4339 * @return {OO.ui.Element} The element, for chaining
4341 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4342 if ( this.pending
=== 0 ) {
4343 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4344 this.updateThemeClasses();
4352 * Decrease the pending counter. The pending state will remain active until the counter is zero
4353 * (i.e., the number of calls to #pushPending and #popPending is the same).
4356 * @return {OO.ui.Element} The element, for chaining
4358 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4359 if ( this.pending
=== 1 ) {
4360 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4361 this.updateThemeClasses();
4363 this.pending
= Math
.max( 0, this.pending
- 1 );
4369 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4370 * in the document (for example, in an OO.ui.Window's $overlay).
4372 * The elements's position is automatically calculated and maintained when window is resized or the
4373 * page is scrolled. If you reposition the container manually, you have to call #position to make
4374 * sure the element is still placed correctly.
4376 * As positioning is only possible when both the element and the container are attached to the DOM
4377 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4378 * the #toggle method to display a floating popup, for example.
4384 * @param {Object} [config] Configuration options
4385 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4386 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4387 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4388 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4389 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4390 * 'top': Align the top edge with $floatableContainer's top edge
4391 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4392 * 'center': Vertically align the center with $floatableContainer's center
4393 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4394 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4395 * 'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
4396 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4397 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4398 * 'center': Horizontally align the center with $floatableContainer's center
4399 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4402 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4403 // Configuration initialization
4404 config
= config
|| {};
4407 this.$floatable
= null;
4408 this.$floatableContainer
= null;
4409 this.$floatableWindow
= null;
4410 this.$floatableClosestScrollable
= null;
4411 this.floatableOutOfView
= false;
4412 this.onFloatableScrollHandler
= this.position
.bind( this );
4413 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4416 this.setFloatableContainer( config
.$floatableContainer
);
4417 this.setFloatableElement( config
.$floatable
|| this.$element
);
4418 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4419 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4420 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ?
4421 true : !!config
.hideWhenOutOfView
;
4427 * Set floatable element.
4429 * If an element is already set, it will be cleaned up before setting up the new element.
4431 * @param {jQuery} $floatable Element to make floatable
4433 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4434 if ( this.$floatable
) {
4435 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4436 this.$floatable
.css( { left
: '', top
: '' } );
4439 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4444 * Set floatable container.
4446 * The element will be positioned relative to the specified container.
4448 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4450 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4451 this.$floatableContainer
= $floatableContainer
;
4452 if ( this.$floatable
) {
4458 * Change how the element is positioned vertically.
4460 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4462 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4463 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4464 throw new Error( 'Invalid value for vertical position: ' + position
);
4466 if ( this.verticalPosition
!== position
) {
4467 this.verticalPosition
= position
;
4468 if ( this.$floatable
) {
4475 * Change how the element is positioned horizontally.
4477 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4479 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4480 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4481 throw new Error( 'Invalid value for horizontal position: ' + position
);
4483 if ( this.horizontalPosition
!== position
) {
4484 this.horizontalPosition
= position
;
4485 if ( this.$floatable
) {
4492 * Toggle positioning.
4494 * Do not turn positioning on until after the element is attached to the DOM and visible.
4496 * @param {boolean} [positioning] Enable positioning, omit to toggle
4498 * @return {OO.ui.Element} The element, for chaining
4500 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4501 var closestScrollableOfContainer
;
4503 if ( !this.$floatable
|| !this.$floatableContainer
) {
4507 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4509 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4510 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4511 this.warnedUnattached
= true;
4514 if ( this.positioning
!== positioning
) {
4515 this.positioning
= positioning
;
4517 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer(
4518 this.$floatableContainer
[ 0 ]
4520 // If the scrollable is the root, we have to listen to scroll events
4521 // on the window because of browser inconsistencies.
4522 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4523 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow(
4524 closestScrollableOfContainer
4528 if ( positioning
) {
4529 this.$floatableWindow
= $( this.getElementWindow() );
4530 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4532 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4533 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4535 // Initial position after visible
4538 if ( this.$floatableWindow
) {
4539 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4540 this.$floatableWindow
= null;
4543 if ( this.$floatableClosestScrollable
) {
4544 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4545 this.$floatableClosestScrollable
= null;
4548 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4556 * Check whether the bottom edge of the given element is within the viewport of the given
4560 * @param {jQuery} $element
4561 * @param {jQuery} $container
4564 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4565 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
,
4566 rightEdgeInBounds
, startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4567 direction
= $element
.css( 'direction' );
4569 elemRect
= $element
[ 0 ].getBoundingClientRect();
4570 if ( $container
[ 0 ] === window
) {
4571 viewportSpacing
= OO
.ui
.getViewportSpacing();
4575 right
: document
.documentElement
.clientWidth
,
4576 bottom
: document
.documentElement
.clientHeight
4578 contRect
.top
+= viewportSpacing
.top
;
4579 contRect
.left
+= viewportSpacing
.left
;
4580 contRect
.right
-= viewportSpacing
.right
;
4581 contRect
.bottom
-= viewportSpacing
.bottom
;
4583 contRect
= $container
[ 0 ].getBoundingClientRect();
4586 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4587 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4588 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4589 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4590 if ( direction
=== 'rtl' ) {
4591 startEdgeInBounds
= rightEdgeInBounds
;
4592 endEdgeInBounds
= leftEdgeInBounds
;
4594 startEdgeInBounds
= leftEdgeInBounds
;
4595 endEdgeInBounds
= rightEdgeInBounds
;
4598 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4601 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4604 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4607 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4611 // The other positioning values are all about being inside the container,
4612 // so in those cases all we care about is that any part of the container is visible.
4613 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4614 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4618 * Check if the floatable is hidden to the user because it was offscreen.
4620 * @return {boolean} Floatable is out of view
4622 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4623 return this.floatableOutOfView
;
4627 * Position the floatable below its container.
4629 * This should only be done when both of them are attached to the DOM and visible.
4632 * @return {OO.ui.Element} The element, for chaining
4634 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4635 if ( !this.positioning
) {
4640 // To continue, some things need to be true:
4641 // The element must actually be in the DOM
4642 this.isElementAttached() && (
4643 // The closest scrollable is the current window
4644 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4645 // OR is an element in the element's DOM
4646 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4649 // Abort early if important parts of the widget are no longer attached to the DOM
4653 this.floatableOutOfView
= this.hideWhenOutOfView
&&
4654 !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4655 if ( this.floatableOutOfView
) {
4656 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4659 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4662 this.$floatable
.css( this.computePosition() );
4664 // We updated the position, so re-evaluate the clipping state.
4665 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4666 // will not notice the need to update itself.)
4667 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here.
4668 // Why does it not listen to the right events in the right places?
4677 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4678 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4679 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4681 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4683 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4684 var isBody
, scrollableX
, scrollableY
, containerPos
,
4685 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4686 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4687 direction
= this.$floatableContainer
.css( 'direction' ),
4688 $offsetParent
= this.$floatable
.offsetParent();
4690 if ( $offsetParent
.is( 'html' ) ) {
4691 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4692 // <html> element, but they do work on the <body>
4693 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4695 isBody
= $offsetParent
.is( 'body' );
4696 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' ||
4697 $offsetParent
.css( 'overflow-x' ) === 'auto';
4698 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' ||
4699 $offsetParent
.css( 'overflow-y' ) === 'auto';
4701 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4702 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4703 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container
4704 // is the body, or if it isn't scrollable
4705 scrollTop
= scrollableY
&& !isBody
?
4706 $offsetParent
.scrollTop() : 0;
4707 scrollLeft
= scrollableX
&& !isBody
?
4708 OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4710 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4711 // if the <body> has a margin
4712 containerPos
= isBody
?
4713 this.$floatableContainer
.offset() :
4714 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4715 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4716 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4717 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4718 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4720 if ( this.verticalPosition
=== 'below' ) {
4721 newPos
.top
= containerPos
.bottom
;
4722 } else if ( this.verticalPosition
=== 'above' ) {
4723 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4724 } else if ( this.verticalPosition
=== 'top' ) {
4725 newPos
.top
= containerPos
.top
;
4726 } else if ( this.verticalPosition
=== 'bottom' ) {
4727 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4728 } else if ( this.verticalPosition
=== 'center' ) {
4729 newPos
.top
= containerPos
.top
+
4730 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4733 if ( this.horizontalPosition
=== 'before' ) {
4734 newPos
.end
= containerPos
.start
;
4735 } else if ( this.horizontalPosition
=== 'after' ) {
4736 newPos
.start
= containerPos
.end
;
4737 } else if ( this.horizontalPosition
=== 'start' ) {
4738 newPos
.start
= containerPos
.start
;
4739 } else if ( this.horizontalPosition
=== 'end' ) {
4740 newPos
.end
= containerPos
.end
;
4741 } else if ( this.horizontalPosition
=== 'center' ) {
4742 newPos
.left
= containerPos
.left
+
4743 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4746 if ( newPos
.start
!== undefined ) {
4747 if ( direction
=== 'rtl' ) {
4748 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4749 $offsetParent
).outerWidth() - newPos
.start
;
4751 newPos
.left
= newPos
.start
;
4753 delete newPos
.start
;
4755 if ( newPos
.end
!== undefined ) {
4756 if ( direction
=== 'rtl' ) {
4757 newPos
.left
= newPos
.end
;
4759 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4760 $offsetParent
).outerWidth() - newPos
.end
;
4765 // Account for scroll position
4766 if ( newPos
.top
!== '' ) {
4767 newPos
.top
+= scrollTop
;
4769 if ( newPos
.bottom
!== '' ) {
4770 newPos
.bottom
-= scrollTop
;
4772 if ( newPos
.left
!== '' ) {
4773 newPos
.left
+= scrollLeft
;
4775 if ( newPos
.right
!== '' ) {
4776 newPos
.right
-= scrollLeft
;
4779 // Account for scrollbar gutter
4780 if ( newPos
.bottom
!== '' ) {
4781 newPos
.bottom
-= horizScrollbarHeight
;
4783 if ( direction
=== 'rtl' ) {
4784 if ( newPos
.left
!== '' ) {
4785 newPos
.left
-= vertScrollbarWidth
;
4788 if ( newPos
.right
!== '' ) {
4789 newPos
.right
-= vertScrollbarWidth
;
4797 * Element that can be automatically clipped to visible boundaries.
4799 * Whenever the element's natural height changes, you have to call
4800 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4801 * clipping correctly.
4803 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4804 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4805 * then #$clippable will be given a fixed reduced height and/or width and will be made
4806 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4807 * but you can build a static footer by setting #$clippableContainer to an element that contains
4808 * #$clippable and the footer.
4814 * @param {Object} [config] Configuration options
4815 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4816 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4817 * omit to use #$clippable
4819 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4820 // Configuration initialization
4821 config
= config
|| {};
4824 this.$clippable
= null;
4825 this.$clippableContainer
= null;
4826 this.clipping
= false;
4827 this.clippedHorizontally
= false;
4828 this.clippedVertically
= false;
4829 this.$clippableScrollableContainer
= null;
4830 this.$clippableScroller
= null;
4831 this.$clippableWindow
= null;
4832 this.idealWidth
= null;
4833 this.idealHeight
= null;
4834 this.onClippableScrollHandler
= this.clip
.bind( this );
4835 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4838 if ( config
.$clippableContainer
) {
4839 this.setClippableContainer( config
.$clippableContainer
);
4841 this.setClippableElement( config
.$clippable
|| this.$element
);
4847 * Set clippable element.
4849 * If an element is already set, it will be cleaned up before setting up the new element.
4851 * @param {jQuery} $clippable Element to make clippable
4853 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4854 if ( this.$clippable
) {
4855 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4856 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4857 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4860 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4865 * Set clippable container.
4867 * This is the container that will be measured when deciding whether to clip. When clipping,
4868 * #$clippable will be resized in order to keep the clippable container fully visible.
4870 * If the clippable container is unset, #$clippable will be used.
4872 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4874 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4875 this.$clippableContainer
= $clippableContainer
;
4876 if ( this.$clippable
) {
4884 * Do not turn clipping on until after the element is attached to the DOM and visible.
4886 * @param {boolean} [clipping] Enable clipping, omit to toggle
4888 * @return {OO.ui.Element} The element, for chaining
4890 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4891 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4893 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4894 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4895 this.warnedUnattached
= true;
4898 if ( this.clipping
!== clipping
) {
4899 this.clipping
= clipping
;
4901 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4902 // If the clippable container is the root, we have to listen to scroll events and check
4903 // jQuery.scrollTop on the window because of browser inconsistencies
4904 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4905 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4906 this.$clippableScrollableContainer
;
4907 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4908 this.$clippableWindow
= $( this.getElementWindow() )
4909 .on( 'resize', this.onClippableWindowResizeHandler
);
4910 // Initial clip after visible
4913 this.$clippable
.css( {
4921 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4923 this.$clippableScrollableContainer
= null;
4924 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4925 this.$clippableScroller
= null;
4926 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4927 this.$clippableWindow
= null;
4935 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4937 * @return {boolean} Element will be clipped to the visible area
4939 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4940 return this.clipping
;
4944 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4946 * @return {boolean} Part of the element is being clipped
4948 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4949 return this.clippedHorizontally
|| this.clippedVertically
;
4953 * Check if the right of the element is being clipped by the nearest scrollable container.
4955 * @return {boolean} Part of the element is being clipped
4957 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4958 return this.clippedHorizontally
;
4962 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4964 * @return {boolean} Part of the element is being clipped
4966 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4967 return this.clippedVertically
;
4971 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4973 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4974 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4976 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4977 this.idealWidth
= width
;
4978 this.idealHeight
= height
;
4980 if ( !this.clipping
) {
4981 // Update dimensions
4982 this.$clippable
.css( { width
: width
, height
: height
} );
4984 // While clipping, idealWidth and idealHeight are not considered
4988 * Return the side of the clippable on which it is "anchored" (aligned to something else).
4989 * ClippableElement will clip the opposite side when reducing element's width.
4991 * Classes that mix in ClippableElement should override this to return 'right' if their
4992 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
4993 * If your class also mixes in FloatableElement, this is handled automatically.
4995 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
4996 * always in pixels, even if they were unset or set to 'auto'.)
4998 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
5000 * @return {string} 'left' or 'right'
5002 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
5003 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
5010 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5011 * ClippableElement will clip the opposite side when reducing element's width.
5013 * Classes that mix in ClippableElement should override this to return 'bottom' if their
5014 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
5015 * If your class also mixes in FloatableElement, this is handled automatically.
5017 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5018 * always in pixels, even if they were unset or set to 'auto'.)
5020 * When in doubt, 'top' is a sane fallback.
5022 * @return {string} 'top' or 'bottom'
5024 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
5025 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
5032 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5033 * when the element's natural height changes.
5035 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5036 * overlapped by, the visible area of the nearest scrollable container.
5038 * Because calling clip() when the natural height changes isn't always possible, we also set
5039 * max-height when the element isn't being clipped. This means that if the element tries to grow
5040 * beyond the edge, something reasonable will happen before clip() is called.
5043 * @return {OO.ui.Element} The element, for chaining
5045 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
5046 var extraHeight
, extraWidth
, viewportSpacing
,
5047 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
5048 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
5049 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
5050 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
5051 // Extra tolerance so that the sloppy code below doesn't result in results that are off
5052 // by one or two pixels. (And also so that we have space to display drop shadows.)
5053 // Chosen by fair dice roll.
5056 if ( !this.clipping
) {
5057 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below
5062 function rectIntersection( a
, b
) {
5064 out
.top
= Math
.max( a
.top
, b
.top
);
5065 out
.left
= Math
.max( a
.left
, b
.left
);
5066 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
5067 out
.right
= Math
.min( a
.right
, b
.right
);
5071 viewportSpacing
= OO
.ui
.getViewportSpacing();
5073 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
5074 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
5075 // Dimensions of the browser window, rather than the element!
5079 right
: document
.documentElement
.clientWidth
,
5080 bottom
: document
.documentElement
.clientHeight
5082 viewportRect
.top
+= viewportSpacing
.top
;
5083 viewportRect
.left
+= viewportSpacing
.left
;
5084 viewportRect
.right
-= viewportSpacing
.right
;
5085 viewportRect
.bottom
-= viewportSpacing
.bottom
;
5087 $viewport
= this.$clippableScrollableContainer
;
5088 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
5089 // Convert into a plain object
5090 viewportRect
= $.extend( {}, viewportRect
);
5093 // Account for scrollbar gutter
5094 direction
= $viewport
.css( 'direction' );
5095 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
5096 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
5097 viewportRect
.bottom
-= horizScrollbarHeight
;
5098 if ( direction
=== 'rtl' ) {
5099 viewportRect
.left
+= vertScrollbarWidth
;
5101 viewportRect
.right
-= vertScrollbarWidth
;
5104 // Add arbitrary tolerance
5105 viewportRect
.top
+= buffer
;
5106 viewportRect
.left
+= buffer
;
5107 viewportRect
.right
-= buffer
;
5108 viewportRect
.bottom
-= buffer
;
5110 $item
= this.$clippableContainer
|| this.$clippable
;
5112 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
5113 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
5115 itemRect
= $item
[ 0 ].getBoundingClientRect();
5116 // Convert into a plain object
5117 itemRect
= $.extend( {}, itemRect
);
5119 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5120 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5121 if ( this.getHorizontalAnchorEdge() === 'right' ) {
5122 itemRect
.left
= viewportRect
.left
;
5124 itemRect
.right
= viewportRect
.right
;
5126 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5127 itemRect
.top
= viewportRect
.top
;
5129 itemRect
.bottom
= viewportRect
.bottom
;
5132 availableRect
= rectIntersection( viewportRect
, itemRect
);
5134 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5135 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5136 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5137 desiredWidth
= Math
.min( desiredWidth
,
5138 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5139 desiredHeight
= Math
.min( desiredHeight
,
5140 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5141 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5142 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5143 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5144 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5145 clipWidth
= allotedWidth
< naturalWidth
;
5146 clipHeight
= allotedHeight
< naturalHeight
;
5149 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5151 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5153 this.$clippable
.css( 'overflowX', 'scroll' );
5154 // eslint-disable-next-line no-void
5155 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5156 this.$clippable
.css( {
5157 width
: Math
.max( 0, allotedWidth
),
5161 this.$clippable
.css( {
5163 width
: this.idealWidth
|| '',
5164 maxWidth
: Math
.max( 0, allotedWidth
)
5168 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5170 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5172 this.$clippable
.css( 'overflowY', 'scroll' );
5173 // eslint-disable-next-line no-void
5174 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5175 this.$clippable
.css( {
5176 height
: Math
.max( 0, allotedHeight
),
5180 this.$clippable
.css( {
5182 height
: this.idealHeight
|| '',
5183 maxHeight
: Math
.max( 0, allotedHeight
)
5187 // If we stopped clipping in at least one of the dimensions
5188 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5189 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5192 this.clippedHorizontally
= clipWidth
;
5193 this.clippedVertically
= clipHeight
;
5199 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5200 * By default, each popup has an anchor that points toward its origin.
5201 * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
5203 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5207 * var popup = new OO.ui.PopupWidget( {
5208 * $content: $( '<p>Hi there!</p>' ),
5213 * $( document.body ).append( popup.$element );
5214 * // To display the popup, toggle the visibility to 'true'.
5215 * popup.toggle( true );
5217 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5220 * @extends OO.ui.Widget
5221 * @mixins OO.ui.mixin.LabelElement
5222 * @mixins OO.ui.mixin.ClippableElement
5223 * @mixins OO.ui.mixin.FloatableElement
5226 * @param {Object} [config] Configuration options
5227 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5228 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5229 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5230 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5231 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5232 * of $floatableContainer
5233 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5234 * of $floatableContainer
5235 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5236 * endwards (right/left) to the vertical center of $floatableContainer
5237 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5238 * startwards (left/right) to the vertical center of $floatableContainer
5239 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5240 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in
5241 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5242 * move the popup as far downwards as possible.
5243 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in
5244 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5245 * move the popup as far upwards as possible.
5246 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the
5247 * center of the popup with the center of $floatableContainer.
5248 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5249 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5250 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5251 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5252 * desired direction to display the popup without clipping
5253 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5254 * See the [OOUI docs on MediaWiki][3] for an example.
5255 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5256 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a
5258 * @cfg {jQuery} [$content] Content to append to the popup's body
5259 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5260 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5261 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5262 * This config option is only relevant if #autoClose is set to `true`. See the
5263 * [OOUI documentation on MediaWiki][2] for an example.
5264 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5265 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5267 * @cfg {boolean} [padded=false] Add padding to the popup's body
5269 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5270 // Configuration initialization
5271 config
= config
|| {};
5273 // Parent constructor
5274 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5276 // Properties (must be set before ClippableElement constructor call)
5277 this.$body
= $( '<div>' );
5278 this.$popup
= $( '<div>' );
5280 // Mixin constructors
5281 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5282 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {
5283 $clippable
: this.$body
,
5284 $clippableContainer
: this.$popup
5286 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5289 this.$anchor
= $( '<div>' );
5290 // If undefined, will be computed lazily in computePosition()
5291 this.$container
= config
.$container
;
5292 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5293 this.autoClose
= !!config
.autoClose
;
5294 this.transitionTimeout
= null;
5295 this.anchored
= false;
5296 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5297 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5300 this.setSize( config
.width
, config
.height
);
5301 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5302 this.setAlignment( config
.align
|| 'center' );
5303 this.setPosition( config
.position
|| 'below' );
5304 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5305 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5306 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5307 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5309 .addClass( 'oo-ui-popupWidget-popup' )
5310 .append( this.$body
);
5312 .addClass( 'oo-ui-popupWidget' )
5313 .append( this.$popup
, this.$anchor
);
5314 // Move content, which was added to #$element by OO.ui.Widget, to the body
5315 // FIXME This is gross, we should use '$body' or something for the config
5316 if ( config
.$content
instanceof $ ) {
5317 this.$body
.append( config
.$content
);
5320 if ( config
.padded
) {
5321 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5324 if ( config
.head
) {
5325 this.closeButton
= new OO
.ui
.ButtonWidget( {
5329 this.closeButton
.connect( this, {
5330 click
: 'onCloseButtonClick'
5332 this.$head
= $( '<div>' )
5333 .addClass( 'oo-ui-popupWidget-head' )
5334 .append( this.$label
, this.closeButton
.$element
);
5335 this.$popup
.prepend( this.$head
);
5338 if ( config
.$footer
) {
5339 this.$footer
= $( '<div>' )
5340 .addClass( 'oo-ui-popupWidget-footer' )
5341 .append( config
.$footer
);
5342 this.$popup
.append( this.$footer
);
5345 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5346 // that reference properties not initialized at that time of parent class construction
5347 // TODO: Find a better way to handle post-constructor setup
5348 this.visible
= false;
5349 this.$element
.addClass( 'oo-ui-element-hidden' );
5354 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5355 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5356 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5357 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5364 * The popup is ready: it is visible and has been positioned and clipped.
5370 * Handles document mouse down events.
5373 * @param {MouseEvent} e Mouse down event
5375 OO
.ui
.PopupWidget
.prototype.onDocumentMouseDown = function ( e
) {
5378 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5380 this.toggle( false );
5385 * Bind document mouse down listener.
5389 OO
.ui
.PopupWidget
.prototype.bindDocumentMouseDownListener = function () {
5390 // Capture clicks outside popup
5391 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5392 // We add 'click' event because iOS safari needs to respond to this event.
5393 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5394 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5395 // of occasionally not emitting 'click' properly, that event seems to be the standard
5396 // that it should be emitting, so we add it to this and will operate the event handler
5397 // on whichever of these events was triggered first
5398 this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5402 * Handles close button click events.
5406 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5407 if ( this.isVisible() ) {
5408 this.toggle( false );
5413 * Unbind document mouse down listener.
5417 OO
.ui
.PopupWidget
.prototype.unbindDocumentMouseDownListener = function () {
5418 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5419 this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5423 * Handles document key down events.
5426 * @param {KeyboardEvent} e Key down event
5428 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5430 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5433 this.toggle( false );
5435 e
.stopPropagation();
5440 * Bind document key down listener.
5444 OO
.ui
.PopupWidget
.prototype.bindDocumentKeyDownListener = function () {
5445 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5449 * Unbind document key down listener.
5453 OO
.ui
.PopupWidget
.prototype.unbindDocumentKeyDownListener = function () {
5454 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5458 * Show, hide, or toggle the visibility of the anchor.
5460 * @param {boolean} [show] Show anchor, omit to toggle
5462 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5463 show
= show
=== undefined ? !this.anchored
: !!show
;
5465 if ( this.anchored
!== show
) {
5467 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5468 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5470 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5471 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5473 this.anchored
= show
;
5478 * Change which edge the anchor appears on.
5480 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5482 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5483 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5484 throw new Error( 'Invalid value for edge: ' + edge
);
5486 if ( this.anchorEdge
!== null ) {
5487 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5489 this.anchorEdge
= edge
;
5490 if ( this.anchored
) {
5491 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5496 * Check if the anchor is visible.
5498 * @return {boolean} Anchor is visible
5500 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5501 return this.anchored
;
5505 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5506 * `.toggle( true )` after its #$element is attached to the DOM.
5508 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5509 * it in the right place and with the right dimensions only work correctly while it is attached.
5510 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5511 * strictly enforced, so currently it only generates a warning in the browser console.
5516 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5517 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5518 show
= show
=== undefined ? !this.isVisible() : !!show
;
5520 change
= show
!== this.isVisible();
5522 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5523 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5524 this.warnedUnattached
= true;
5526 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5527 // Fall back to the parent node if the floatableContainer is not set
5528 this.setFloatableContainer( this.$element
.parent() );
5531 if ( change
&& show
&& this.autoFlip
) {
5532 // Reset auto-flipping before showing the popup again. It's possible we no longer need to
5533 // flip (e.g. if the user scrolled).
5534 this.isAutoFlipped
= false;
5538 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5541 this.togglePositioning( show
&& !!this.$floatableContainer
);
5544 if ( this.autoClose
) {
5545 this.bindDocumentMouseDownListener();
5546 this.bindDocumentKeyDownListener();
5548 this.updateDimensions();
5549 this.toggleClipping( true );
5551 if ( this.autoFlip
) {
5552 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5553 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5554 // If opening the popup in the normal direction causes it to be clipped,
5555 // open in the opposite one instead
5556 normalHeight
= this.$element
.height();
5557 this.isAutoFlipped
= !this.isAutoFlipped
;
5559 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5560 // If that also causes it to be clipped, open in whichever direction
5561 // we have more space
5562 oppositeHeight
= this.$element
.height();
5563 if ( oppositeHeight
< normalHeight
) {
5564 this.isAutoFlipped
= !this.isAutoFlipped
;
5570 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5571 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5572 // If opening the popup in the normal direction causes it to be clipped,
5573 // open in the opposite one instead
5574 normalWidth
= this.$element
.width();
5575 this.isAutoFlipped
= !this.isAutoFlipped
;
5576 // Due to T180173 horizontally clipped PopupWidgets have messed up
5577 // dimensions, which causes positioning to be off. Toggle clipping back and
5578 // forth to work around.
5579 this.toggleClipping( false );
5581 this.toggleClipping( true );
5582 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5583 // If that also causes it to be clipped, open in whichever direction
5584 // we have more space
5585 oppositeWidth
= this.$element
.width();
5586 if ( oppositeWidth
< normalWidth
) {
5587 this.isAutoFlipped
= !this.isAutoFlipped
;
5588 // Due to T180173, horizontally clipped PopupWidgets have messed up
5589 // dimensions, which causes positioning to be off. Toggle clipping
5590 // back and forth to work around.
5591 this.toggleClipping( false );
5593 this.toggleClipping( true );
5600 this.emit( 'ready' );
5602 this.toggleClipping( false );
5603 if ( this.autoClose
) {
5604 this.unbindDocumentMouseDownListener();
5605 this.unbindDocumentKeyDownListener();
5614 * Set the size of the popup.
5616 * Changing the size may also change the popup's position depending on the alignment.
5618 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5619 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5620 * @param {boolean} [transition=false] Use a smooth transition
5623 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5624 this.width
= width
!== undefined ? width
: 320;
5625 this.height
= height
!== undefined ? height
: null;
5626 if ( this.isVisible() ) {
5627 this.updateDimensions( transition
);
5632 * Update the size and position.
5634 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5635 * be called automatically.
5637 * @param {boolean} [transition=false] Use a smooth transition
5640 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5643 // Prevent transition from being interrupted
5644 clearTimeout( this.transitionTimeout
);
5646 // Enable transition
5647 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5653 // Prevent transitioning after transition is complete
5654 this.transitionTimeout
= setTimeout( function () {
5655 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5658 // Prevent transitioning immediately
5659 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5666 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5667 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
,
5668 anchorPos
, anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
,
5669 floatablePos
, offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5671 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5672 popupPositionOppositeMap
= {
5680 'force-left': 'backwards',
5681 'force-right': 'forwards'
5684 'force-left': 'forwards',
5685 'force-right': 'backwards'
5697 backwards
: this.anchored
? 'before' : 'end'
5705 if ( !this.$container
) {
5706 // Lazy-initialize $container if not specified in constructor
5707 this.$container
= $( this.getClosestScrollableElementContainer() );
5709 direction
= this.$container
.css( 'direction' );
5711 // Set height and width before we do anything else, since it might cause our measurements
5712 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5714 width
: this.width
!== null ? this.width
: 'auto',
5715 height
: this.height
!== null ? this.height
: 'auto'
5718 align
= alignMap
[ direction
][ this.align
] || this.align
;
5719 popupPosition
= this.popupPosition
;
5720 if ( this.isAutoFlipped
) {
5721 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5724 // If the popup is positioned before or after, then the anchor positioning is vertical,
5725 // otherwise horizontal
5726 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5727 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5728 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5729 near
= vertical
? 'top' : 'left';
5730 far
= vertical
? 'bottom' : 'right';
5731 sizeProp
= vertical
? 'Height' : 'Width';
5732 popupSize
= vertical
?
5733 ( this.height
|| this.$popup
.height() ) :
5734 ( this.width
|| this.$popup
.width() );
5736 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5737 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5738 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5741 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5742 // Find out which property FloatableElement used for positioning, and adjust that value
5743 positionProp
= vertical
?
5744 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5745 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5747 // Figure out where the near and far edges of the popup and $floatableContainer are
5748 floatablePos
= this.$floatableContainer
.offset();
5749 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5750 // Measure where the offsetParent is and compute our position based on that and parentPosition
5751 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5752 { top
: 0, left
: 0 } :
5753 this.$element
.offsetParent().offset();
5755 if ( positionProp
=== near
) {
5756 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5757 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5759 popupPos
[ far
] = offsetParentPos
[ near
] +
5760 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5761 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5764 if ( this.anchored
) {
5765 // Position the anchor (which is positioned relative to the popup) to point to
5766 // $floatableContainer
5767 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5768 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5770 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more
5771 // space this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use
5772 // scrollWidth/Height
5773 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5774 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5775 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5776 // Not enough space for the anchor on the start side; pull the popup startwards
5777 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5778 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5779 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5780 // Not enough space for the anchor on the end side; pull the popup endwards
5781 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5782 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5784 positionAdjustment
= 0;
5787 positionAdjustment
= 0;
5790 // Check if the popup will go beyond the edge of this.$container
5791 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5792 { top
: 0, left
: 0 } :
5793 this.$container
.offset();
5794 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5795 if ( this.$container
[ 0 ] === document
.documentElement
) {
5796 viewportSpacing
= OO
.ui
.getViewportSpacing();
5797 containerPos
[ near
] += viewportSpacing
[ near
];
5798 containerPos
[ far
] -= viewportSpacing
[ far
];
5800 // Take into account how much the popup will move because of the adjustments we're going to make
5801 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5802 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5803 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5804 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5805 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5806 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5807 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5808 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5809 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5810 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5813 if ( this.anchored
) {
5814 // Adjust anchorOffset for positionAdjustment
5815 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5817 // Position the anchor
5818 anchorCss
[ start
] = anchorOffset
;
5819 this.$anchor
.css( anchorCss
);
5822 // Move the popup if needed
5823 parentPosition
[ positionProp
] += positionAdjustment
;
5825 return parentPosition
;
5829 * Set popup alignment
5831 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5832 * `backwards` or `forwards`.
5834 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5835 // Validate alignment
5836 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5839 this.align
= 'center';
5845 * Get popup alignment
5847 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5848 * `backwards` or `forwards`.
5850 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5855 * Change the positioning of the popup.
5857 * @param {string} position 'above', 'below', 'before' or 'after'
5859 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5860 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5863 this.popupPosition
= position
;
5868 * Get popup positioning.
5870 * @return {string} 'above', 'below', 'before' or 'after'
5872 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5873 return this.popupPosition
;
5877 * Set popup auto-flipping.
5879 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5880 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5881 * desired direction to display the popup without clipping
5883 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5884 autoFlip
= !!autoFlip
;
5886 if ( this.autoFlip
!== autoFlip
) {
5887 this.autoFlip
= autoFlip
;
5892 * Set which elements will not close the popup when clicked.
5894 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
5896 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
5898 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
5899 this.$autoCloseIgnore
= $autoCloseIgnore
;
5903 * Get an ID of the body element, this can be used as the
5904 * `aria-describedby` attribute for an input field.
5906 * @return {string} The ID of the body element
5908 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5909 var id
= this.$body
.attr( 'id' );
5910 if ( id
=== undefined ) {
5911 id
= OO
.ui
.generateElementId();
5912 this.$body
.attr( 'id', id
);
5918 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5919 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5920 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5921 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5927 * @param {Object} [config] Configuration options
5928 * @cfg {Object} [popup] Configuration to pass to popup
5929 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5931 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5932 // Configuration initialization
5933 config
= config
|| {};
5936 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5939 $floatableContainer
: this.$element
5943 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5953 * @return {OO.ui.PopupWidget} Popup widget
5955 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5960 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5961 * which is used to display additional information or options.
5964 * // A PopupButtonWidget.
5965 * var popupButton = new OO.ui.PopupButtonWidget( {
5966 * label: 'Popup button with options',
5969 * $content: $( '<p>Additional options here.</p>' ),
5971 * align: 'force-left'
5974 * // Append the button to the DOM.
5975 * $( document.body ).append( popupButton.$element );
5978 * @extends OO.ui.ButtonWidget
5979 * @mixins OO.ui.mixin.PopupElement
5982 * @param {Object} [config] Configuration options
5983 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful
5984 * in cases where the expanded popup is larger than its containing `<div>`. The specified overlay
5985 * layer is usually on top of the containing `<div>` and has a larger area. By default, the popup
5986 * uses relative positioning.
5987 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
5989 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
5990 // Configuration initialization
5991 config
= config
|| {};
5993 // Parent constructor
5994 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
5996 // Mixin constructors
5997 OO
.ui
.mixin
.PopupElement
.call( this, config
);
6000 this.$overlay
= ( config
.$overlay
=== true ?
6001 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
6004 this.connect( this, {
6009 this.$element
.addClass( 'oo-ui-popupButtonWidget' );
6011 .addClass( 'oo-ui-popupButtonWidget-popup' )
6012 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6013 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6014 this.$overlay
.append( this.popup
.$element
);
6019 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
6020 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
6025 * Handle the button action being triggered.
6029 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
6030 this.popup
.toggle();
6034 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6036 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6041 * @mixins OO.ui.mixin.GroupElement
6044 * @param {Object} [config] Configuration options
6046 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
6047 // Mixin constructors
6048 OO
.ui
.mixin
.GroupElement
.call( this, config
);
6053 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
6058 * Set the disabled state of the widget.
6060 * This will also update the disabled state of child widgets.
6062 * @param {boolean} disabled Disable widget
6064 * @return {OO.ui.Widget} The widget, for chaining
6066 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
6070 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6071 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
6073 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6075 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6076 this.items
[ i
].updateDisabled();
6084 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6086 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group.
6087 * This allows bidirectional communication.
6089 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6097 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
6104 * Check if widget is disabled.
6106 * Checks parent if present, making disabled state inheritable.
6108 * @return {boolean} Widget is disabled
6110 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
6111 return this.disabled
||
6112 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
6116 * Set group element is in.
6118 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6120 * @return {OO.ui.Widget} The widget, for chaining
6122 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
6124 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6125 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
6127 // Initialize item disabled states
6128 this.updateDisabled();
6134 * OptionWidgets are special elements that can be selected and configured with data. The
6135 * data is often unique for each option, but it does not have to be. OptionWidgets are used
6136 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6137 * and examples, please see the [OOUI documentation on MediaWiki][1].
6139 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6142 * @extends OO.ui.Widget
6143 * @mixins OO.ui.mixin.ItemWidget
6144 * @mixins OO.ui.mixin.LabelElement
6145 * @mixins OO.ui.mixin.FlaggedElement
6146 * @mixins OO.ui.mixin.AccessKeyedElement
6147 * @mixins OO.ui.mixin.TitledElement
6150 * @param {Object} [config] Configuration options
6152 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
6153 // Configuration initialization
6154 config
= config
|| {};
6156 // Parent constructor
6157 OO
.ui
.OptionWidget
.parent
.call( this, config
);
6159 // Mixin constructors
6160 OO
.ui
.mixin
.ItemWidget
.call( this );
6161 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6162 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6163 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6164 OO
.ui
.mixin
.TitledElement
.call( this, config
);
6167 this.highlighted
= false;
6168 this.pressed
= false;
6169 this.setSelected( !!config
.selected
);
6173 .data( 'oo-ui-optionWidget', this )
6174 // Allow programmatic focussing (and by access key), but not tabbing
6175 .attr( 'tabindex', '-1' )
6176 .attr( 'role', 'option' )
6177 .addClass( 'oo-ui-optionWidget' )
6178 .append( this.$label
);
6183 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6184 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6185 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6186 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6187 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6188 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.TitledElement
);
6190 /* Static Properties */
6193 * Whether this option can be selected. See #setSelected.
6197 * @property {boolean}
6199 OO
.ui
.OptionWidget
.static.selectable
= true;
6202 * Whether this option can be highlighted. See #setHighlighted.
6206 * @property {boolean}
6208 OO
.ui
.OptionWidget
.static.highlightable
= true;
6211 * Whether this option can be pressed. See #setPressed.
6215 * @property {boolean}
6217 OO
.ui
.OptionWidget
.static.pressable
= true;
6220 * Whether this option will be scrolled into view when it is selected.
6224 * @property {boolean}
6226 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6231 * Check if the option can be selected.
6233 * @return {boolean} Item is selectable
6235 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6236 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6240 * Check if the option can be highlighted. A highlight indicates that the option
6241 * may be selected when a user presses Enter key or clicks. Disabled items cannot
6244 * @return {boolean} Item is highlightable
6246 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6247 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6251 * Check if the option can be pressed. The pressed state occurs when a user mouses
6252 * down on an item, but has not yet let go of the mouse.
6254 * @return {boolean} Item is pressable
6256 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6257 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6261 * Check if the option is selected.
6263 * @return {boolean} Item is selected
6265 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6266 return this.selected
;
6270 * Check if the option is highlighted. A highlight indicates that the
6271 * item may be selected when a user presses Enter key or clicks.
6273 * @return {boolean} Item is highlighted
6275 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6276 return this.highlighted
;
6280 * Check if the option is pressed. The pressed state occurs when a user mouses
6281 * down on an item, but has not yet let go of the mouse. The item may appear
6282 * selected, but it will not be selected until the user releases the mouse.
6284 * @return {boolean} Item is pressed
6286 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6287 return this.pressed
;
6291 * Set the option’s selected state. In general, all modifications to the selection
6292 * should be handled by the SelectWidget’s
6293 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
6295 * @param {boolean} [state=false] Select option
6297 * @return {OO.ui.Widget} The widget, for chaining
6299 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6300 if ( this.constructor.static.selectable
) {
6301 this.selected
= !!state
;
6303 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6304 .attr( 'aria-selected', state
.toString() );
6305 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6306 this.scrollElementIntoView();
6308 this.updateThemeClasses();
6314 * Set the option’s highlighted state. In general, all programmatic
6315 * modifications to the highlight should be handled by the
6316 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6317 * method instead of this method.
6319 * @param {boolean} [state=false] Highlight option
6321 * @return {OO.ui.Widget} The widget, for chaining
6323 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6324 if ( this.constructor.static.highlightable
) {
6325 this.highlighted
= !!state
;
6326 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6327 this.updateThemeClasses();
6333 * Set the option’s pressed state. In general, all
6334 * programmatic modifications to the pressed state should be handled by the
6335 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6336 * method instead of this method.
6338 * @param {boolean} [state=false] Press option
6340 * @return {OO.ui.Widget} The widget, for chaining
6342 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6343 if ( this.constructor.static.pressable
) {
6344 this.pressed
= !!state
;
6345 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6346 this.updateThemeClasses();
6352 * Get text to match search strings against.
6354 * The default implementation returns the label text, but subclasses
6355 * can override this to provide more complex behavior.
6357 * @return {string|boolean} String to match search string against
6359 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6360 var label
= this.getLabel();
6361 return typeof label
=== 'string' ? label
: this.$label
.text();
6365 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6366 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6367 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6370 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For
6371 * more information, please see the [OOUI documentation on MediaWiki][1].
6374 * // A select widget with three options.
6375 * var select = new OO.ui.SelectWidget( {
6377 * new OO.ui.OptionWidget( {
6379 * label: 'Option One',
6381 * new OO.ui.OptionWidget( {
6383 * label: 'Option Two',
6385 * new OO.ui.OptionWidget( {
6387 * label: 'Option Three',
6391 * $( document.body ).append( select.$element );
6393 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6397 * @extends OO.ui.Widget
6398 * @mixins OO.ui.mixin.GroupWidget
6401 * @param {Object} [config] Configuration options
6402 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6403 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6404 * the [OOUI documentation on MediaWiki] [2] for examples.
6405 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6406 * @cfg {boolean} [multiselect] Allow for multiple selections
6408 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6409 // Configuration initialization
6410 config
= config
|| {};
6412 // Parent constructor
6413 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6415 // Mixin constructors
6416 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {
6417 $group
: this.$element
6421 this.pressed
= false;
6422 this.selecting
= null;
6423 this.multiselect
= !!config
.multiselect
;
6424 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
6425 this.onDocumentMouseMoveHandler
= this.onDocumentMouseMove
.bind( this );
6426 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
6427 this.onDocumentKeyPressHandler
= this.onDocumentKeyPress
.bind( this );
6428 this.keyPressBuffer
= '';
6429 this.keyPressBufferTimer
= null;
6430 this.blockMouseOverEvents
= 0;
6433 this.connect( this, {
6437 focusin
: this.onFocus
.bind( this ),
6438 mousedown
: this.onMouseDown
.bind( this ),
6439 mouseover
: this.onMouseOver
.bind( this ),
6440 mouseleave
: this.onMouseLeave
.bind( this )
6445 // -depressed is a deprecated alias of -unpressed
6446 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-unpressed oo-ui-selectWidget-depressed' )
6447 .attr( 'role', 'listbox' );
6448 this.setFocusOwner( this.$element
);
6449 if ( Array
.isArray( config
.items
) ) {
6450 this.addItems( config
.items
);
6456 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6457 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6464 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6466 * @param {OO.ui.OptionWidget|null} item Highlighted item
6472 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6473 * pressed state of an option.
6475 * @param {OO.ui.OptionWidget|null} item Pressed item
6481 * A `select` event is emitted when the selection is modified programmatically with the #selectItem
6484 * @param {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} items Currently selected items
6490 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6492 * @param {OO.ui.OptionWidget} item Chosen item
6493 * @param {boolean} selected Item is selected
6499 * An `add` event is emitted when options are added to the select with the #addItems method.
6501 * @param {OO.ui.OptionWidget[]} items Added items
6502 * @param {number} index Index of insertion point
6508 * A `remove` event is emitted when options are removed from the select with the #clearItems
6509 * or #removeItems methods.
6511 * @param {OO.ui.OptionWidget[]} items Removed items
6514 /* Static methods */
6517 * Normalize text for filter matching
6519 * @param {string} text Text
6520 * @return {string} Normalized text
6522 OO
.ui
.SelectWidget
.static.normalizeForMatching = function ( text
) {
6523 // Replace trailing whitespace, normalize multiple spaces and make case insensitive
6524 var normalized
= text
.trim().replace( /\s+/, ' ' ).toLowerCase();
6526 // Normalize Unicode
6527 // eslint-disable-next-line no-restricted-properties
6528 if ( normalized
.normalize
) {
6529 // eslint-disable-next-line no-restricted-properties
6530 normalized
= normalized
.normalize();
6538 * Handle focus events
6541 * @param {jQuery.Event} event
6543 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6545 if ( event
.target
=== this.$element
[ 0 ] ) {
6546 // This widget was focussed, e.g. by the user tabbing to it.
6547 // The styles for focus state depend on one of the items being selected.
6548 if ( !this.findSelectedItem() ) {
6549 item
= this.findFirstSelectableItem();
6552 if ( event
.target
.tabIndex
=== -1 ) {
6553 // One of the options got focussed (and the event bubbled up here).
6554 // They can't be tabbed to, but they can be activated using access keys.
6555 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6556 item
= this.findTargetItem( event
);
6558 // There is something actually user-focusable in one of the labels of the options, and
6559 // the user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change
6566 if ( item
.constructor.static.highlightable
) {
6567 this.highlightItem( item
);
6569 this.selectItem( item
);
6573 if ( event
.target
!== this.$element
[ 0 ] ) {
6574 this.$focusOwner
.trigger( 'focus' );
6579 * Handle mouse down events.
6582 * @param {jQuery.Event} e Mouse down event
6583 * @return {undefined/boolean} False to prevent default if event is handled
6585 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6588 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6589 this.togglePressed( true );
6590 item
= this.findTargetItem( e
);
6591 if ( item
&& item
.isSelectable() ) {
6592 this.pressItem( item
);
6593 this.selecting
= item
;
6594 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6595 this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6602 * Handle document mouse up events.
6605 * @param {MouseEvent} e Mouse up event
6606 * @return {undefined/boolean} False to prevent default if event is handled
6608 OO
.ui
.SelectWidget
.prototype.onDocumentMouseUp = function ( e
) {
6611 this.togglePressed( false );
6612 if ( !this.selecting
) {
6613 item
= this.findTargetItem( e
);
6614 if ( item
&& item
.isSelectable() ) {
6615 this.selecting
= item
;
6618 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6619 this.pressItem( null );
6620 this.chooseItem( this.selecting
);
6621 this.selecting
= null;
6624 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6625 this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6631 * Handle document mouse move events.
6634 * @param {MouseEvent} e Mouse move event
6636 OO
.ui
.SelectWidget
.prototype.onDocumentMouseMove = function ( e
) {
6639 if ( !this.isDisabled() && this.pressed
) {
6640 item
= this.findTargetItem( e
);
6641 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6642 this.pressItem( item
);
6643 this.selecting
= item
;
6649 * Handle mouse over events.
6652 * @param {jQuery.Event} e Mouse over event
6653 * @return {undefined/boolean} False to prevent default if event is handled
6655 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6657 if ( this.blockMouseOverEvents
) {
6660 if ( !this.isDisabled() ) {
6661 item
= this.findTargetItem( e
);
6662 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6668 * Handle mouse leave events.
6671 * @param {jQuery.Event} e Mouse over event
6672 * @return {undefined/boolean} False to prevent default if event is handled
6674 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6675 if ( !this.isDisabled() ) {
6676 this.highlightItem( null );
6682 * Handle document key down events.
6685 * @param {KeyboardEvent} e Key down event
6687 OO
.ui
.SelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
6690 currentItem
= this.findHighlightedItem(),
6691 firstItem
= this.getItems()[ 0 ];
6693 if ( !this.isDisabled() && this.isVisible() ) {
6694 switch ( e
.keyCode
) {
6695 case OO
.ui
.Keys
.ENTER
:
6696 if ( currentItem
) {
6697 // Was only highlighted, now let's select it. No-op if already selected.
6698 this.chooseItem( currentItem
);
6703 case OO
.ui
.Keys
.LEFT
:
6704 this.clearKeyPressBuffer();
6705 nextItem
= currentItem
? this.findRelativeSelectableItem( currentItem
, -1 ) : firstItem
;
6708 case OO
.ui
.Keys
.DOWN
:
6709 case OO
.ui
.Keys
.RIGHT
:
6710 this.clearKeyPressBuffer();
6711 nextItem
= currentItem
? this.findRelativeSelectableItem( currentItem
, 1 ) : firstItem
;
6714 case OO
.ui
.Keys
.ESCAPE
:
6715 case OO
.ui
.Keys
.TAB
:
6716 if ( currentItem
) {
6717 currentItem
.setHighlighted( false );
6719 this.unbindDocumentKeyDownListener();
6720 this.unbindDocumentKeyPressListener();
6721 // Don't prevent tabbing away / defocusing
6727 if ( nextItem
.constructor.static.highlightable
) {
6728 this.highlightItem( nextItem
);
6730 this.chooseItem( nextItem
);
6732 this.scrollItemIntoView( nextItem
);
6737 e
.stopPropagation();
6743 * Bind document key down listener.
6747 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyDownListener = function () {
6748 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6752 * Unbind document key down listener.
6756 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
6757 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6761 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6763 * @param {OO.ui.OptionWidget} item Item to scroll into view
6765 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6767 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic
6768 // scrolling and around 100-150 ms after it is finished.
6769 this.blockMouseOverEvents
++;
6770 item
.scrollElementIntoView().done( function () {
6771 setTimeout( function () {
6772 widget
.blockMouseOverEvents
--;
6778 * Clear the key-press buffer
6782 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6783 if ( this.keyPressBufferTimer
) {
6784 clearTimeout( this.keyPressBufferTimer
);
6785 this.keyPressBufferTimer
= null;
6787 this.keyPressBuffer
= '';
6791 * Handle key press events.
6794 * @param {KeyboardEvent} e Key press event
6795 * @return {undefined/boolean} False to prevent default if event is handled
6797 OO
.ui
.SelectWidget
.prototype.onDocumentKeyPress = function ( e
) {
6798 var c
, filter
, item
;
6800 if ( !e
.charCode
) {
6801 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6802 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6807 // eslint-disable-next-line no-restricted-properties
6808 if ( String
.fromCodePoint
) {
6809 // eslint-disable-next-line no-restricted-properties
6810 c
= String
.fromCodePoint( e
.charCode
);
6812 c
= String
.fromCharCode( e
.charCode
);
6815 if ( this.keyPressBufferTimer
) {
6816 clearTimeout( this.keyPressBufferTimer
);
6818 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6820 item
= this.findHighlightedItem() || this.findSelectedItem();
6822 if ( this.keyPressBuffer
=== c
) {
6823 // Common (if weird) special case: typing "xxxx" will cycle through all
6824 // the items beginning with "x".
6826 item
= this.findRelativeSelectableItem( item
, 1 );
6829 this.keyPressBuffer
+= c
;
6832 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6833 if ( !item
|| !filter( item
) ) {
6834 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6837 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6838 this.highlightItem( item
);
6840 this.chooseItem( item
);
6842 this.scrollItemIntoView( item
);
6846 e
.stopPropagation();
6850 * Get a matcher for the specific string
6853 * @param {string} query String to match against items
6854 * @param {string} [mode='prefix'] Matching mode: 'substring', 'prefix', or 'exact'
6855 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6857 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( query
, mode
) {
6858 var normalizeForMatching
= this.constructor.static.normalizeForMatching
,
6859 normalizedQuery
= normalizeForMatching( query
);
6861 // Support deprecated exact=true argument
6862 if ( mode
=== true ) {
6866 return function ( item
) {
6867 var matchText
= normalizeForMatching( item
.getMatchText() );
6869 if ( normalizedQuery
=== '' ) {
6870 // Empty string matches all, except if we are in 'exact'
6871 // mode, where it doesn't match at all
6872 return mode
!== 'exact';
6877 return matchText
=== normalizedQuery
;
6879 return matchText
.indexOf( normalizedQuery
) !== -1;
6882 return matchText
.indexOf( normalizedQuery
) === 0;
6888 * Bind document key press listener.
6892 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyPressListener = function () {
6893 this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6897 * Unbind document key down listener.
6899 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6904 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
6905 this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6906 this.clearKeyPressBuffer();
6910 * Visibility change handler
6913 * @param {boolean} visible
6915 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6917 this.clearKeyPressBuffer();
6922 * Get the closest item to a jQuery.Event.
6925 * @param {jQuery.Event} e
6926 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6928 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6929 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6930 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6933 return $option
.data( 'oo-ui-optionWidget' ) || null;
6937 * Find all selected items, if there are any. If the widget allows for multiselect
6938 * it will return an array of selected options. If the widget doesn't allow for
6939 * multiselect, it will return the selected option or null if no item is selected.
6941 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
6942 * then return an array of selected items (or empty array),
6943 * if the widget is not multiselect, return a single selected item, or `null`
6944 * if no item is selected
6946 OO
.ui
.SelectWidget
.prototype.findSelectedItems = function () {
6947 var selected
= this.items
.filter( function ( item
) {
6948 return item
.isSelected();
6951 return this.multiselect
?
6953 selected
[ 0 ] || null;
6957 * Find selected item.
6959 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
6960 * then return an array of selected items (or empty array),
6961 * if the widget is not multiselect, return a single selected item, or `null`
6962 * if no item is selected
6964 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
6965 return this.findSelectedItems();
6969 * Find highlighted item.
6971 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6973 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6976 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6977 if ( this.items
[ i
].isHighlighted() ) {
6978 return this.items
[ i
];
6985 * Toggle pressed state.
6987 * Press is a state that occurs when a user mouses down on an item, but
6988 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
6989 * until the user releases the mouse.
6991 * @param {boolean} pressed An option is being pressed
6993 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
6994 if ( pressed
=== undefined ) {
6995 pressed
= !this.pressed
;
6997 if ( pressed
!== this.pressed
) {
6999 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
7000 // -depressed is a deprecated alias of -unpressed
7001 .toggleClass( 'oo-ui-selectWidget-unpressed oo-ui-selectWidget-depressed', !pressed
);
7002 this.pressed
= pressed
;
7007 * Highlight an option. If the `item` param is omitted, no options will be highlighted
7008 * and any existing highlight will be removed. The highlight is mutually exclusive.
7010 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
7013 * @return {OO.ui.Widget} The widget, for chaining
7015 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
7016 var i
, len
, highlighted
,
7019 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7020 highlighted
= this.items
[ i
] === item
;
7021 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
7022 this.items
[ i
].setHighlighted( highlighted
);
7028 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7030 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7032 this.emit( 'highlight', item
);
7039 * Fetch an item by its label.
7041 * @param {string} label Label of the item to select.
7042 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7043 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7045 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
7047 len
= this.items
.length
,
7048 filter
= this.getItemMatcher( label
, 'exact' );
7050 for ( i
= 0; i
< len
; i
++ ) {
7051 item
= this.items
[ i
];
7052 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7059 filter
= this.getItemMatcher( label
, 'prefix' );
7060 for ( i
= 0; i
< len
; i
++ ) {
7061 item
= this.items
[ i
];
7062 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7078 * Programmatically select an option by its label. If the item does not exist,
7079 * all options will be deselected.
7081 * @param {string} [label] Label of the item to select.
7082 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7085 * @return {OO.ui.Widget} The widget, for chaining
7087 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
7088 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
7089 if ( label
=== undefined || !itemFromLabel
) {
7090 return this.selectItem();
7092 return this.selectItem( itemFromLabel
);
7096 * Programmatically select an option by its data. If the `data` parameter is omitted,
7097 * or if the item does not exist, all options will be deselected.
7099 * @param {Object|string} [data] Value of the item to select, omit to deselect all
7102 * @return {OO.ui.Widget} The widget, for chaining
7104 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
7105 var itemFromData
= this.findItemFromData( data
);
7106 if ( data
=== undefined || !itemFromData
) {
7107 return this.selectItem();
7109 return this.selectItem( itemFromData
);
7113 * Programmatically unselect an option by its reference. If the widget
7114 * allows for multiple selections, there may be other items still selected;
7115 * otherwise, no items will be selected.
7116 * If no item is given, all selected items will be unselected.
7118 * @param {OO.ui.OptionWidget} [item] Item to unselect
7121 * @return {OO.ui.Widget} The widget, for chaining
7123 OO
.ui
.SelectWidget
.prototype.unselectItem = function ( item
) {
7125 item
.setSelected( false );
7127 this.items
.forEach( function ( item
) {
7128 item
.setSelected( false );
7132 this.emit( 'select', this.findSelectedItems() );
7137 * Programmatically select an option by its reference. If the `item` parameter is omitted,
7138 * all options will be deselected.
7140 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7143 * @return {OO.ui.Widget} The widget, for chaining
7145 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
7146 var i
, len
, selected
,
7149 if ( this.multiselect
&& item
) {
7150 // Select the item directly
7151 item
.setSelected( true );
7153 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7154 selected
= this.items
[ i
] === item
;
7155 if ( this.items
[ i
].isSelected() !== selected
) {
7156 this.items
[ i
].setSelected( selected
);
7162 // TODO: When should a non-highlightable element be selected?
7163 if ( item
&& !item
.constructor.static.highlightable
) {
7165 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7167 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7170 this.emit( 'select', this.findSelectedItems() );
7179 * Press is a state that occurs when a user mouses down on an item, but has not
7180 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7181 * releases the mouse.
7183 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7186 * @return {OO.ui.Widget} The widget, for chaining
7188 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
7189 var i
, len
, pressed
,
7192 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7193 pressed
= this.items
[ i
] === item
;
7194 if ( this.items
[ i
].isPressed() !== pressed
) {
7195 this.items
[ i
].setPressed( pressed
);
7200 this.emit( 'press', item
);
7209 * Note that ‘choose’ should never be modified programmatically. A user can choose
7210 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7211 * use the #selectItem method.
7213 * This method is identical to #selectItem, but may vary in subclasses that take additional action
7214 * when users choose an item with the keyboard or mouse.
7216 * @param {OO.ui.OptionWidget} item Item to choose
7219 * @return {OO.ui.Widget} The widget, for chaining
7221 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
7223 if ( this.multiselect
&& item
.isSelected() ) {
7224 this.unselectItem( item
);
7226 this.selectItem( item
);
7229 this.emit( 'choose', item
, item
.isSelected() );
7236 * Find an option by its position relative to the specified item (or to the start of the option
7237 * array, if item is `null`). The direction in which to search through the option array is specified
7238 * with a number: -1 for reverse (the default) or 1 for forward. The method will return an option,
7239 * or `null` if there are no options in the array.
7241 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at
7242 * the beginning of the array.
7243 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7244 * @param {Function} [filter] Only consider items for which this function returns
7245 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
7246 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
7248 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
7249 var currentIndex
, nextIndex
, i
,
7250 increase
= direction
> 0 ? 1 : -1,
7251 len
= this.items
.length
;
7253 if ( item
instanceof OO
.ui
.OptionWidget
) {
7254 currentIndex
= this.items
.indexOf( item
);
7255 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7257 // If no item is selected and moving forward, start at the beginning.
7258 // If moving backward, start at the end.
7259 nextIndex
= direction
> 0 ? 0 : len
- 1;
7262 for ( i
= 0; i
< len
; i
++ ) {
7263 item
= this.items
[ nextIndex
];
7265 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7266 ( !filter
|| filter( item
) )
7270 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7276 * Find the next selectable item or `null` if there are no selectable items.
7277 * Disabled options and menu-section markers and breaks are not selectable.
7279 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7281 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7282 return this.findRelativeSelectableItem( null, 1 );
7286 * Add an array of options to the select. Optionally, an index number can be used to
7287 * specify an insertion point.
7289 * @param {OO.ui.OptionWidget[]} items Items to add
7290 * @param {number} [index] Index to insert items after
7293 * @return {OO.ui.Widget} The widget, for chaining
7295 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7297 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7299 // Always provide an index, even if it was omitted
7300 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7306 * Remove the specified array of options from the select. Options will be detached
7307 * from the DOM, not removed, so they can be reused later. To remove all options from
7308 * the select, you may wish to use the #clearItems method instead.
7310 * @param {OO.ui.OptionWidget[]} items Items to remove
7313 * @return {OO.ui.Widget} The widget, for chaining
7315 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7318 // Deselect items being removed
7319 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7321 if ( item
.isSelected() ) {
7322 this.selectItem( null );
7327 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7329 this.emit( 'remove', items
);
7335 * Clear all options from the select. Options will be detached from the DOM, not removed,
7336 * so that they can be reused later. To remove a subset of options from the select, use
7337 * the #removeItems method.
7341 * @return {OO.ui.Widget} The widget, for chaining
7343 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7344 var items
= this.items
.slice();
7347 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7350 this.selectItem( null );
7352 this.emit( 'remove', items
);
7358 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7360 * This is used to set `aria-activedescendant` and `aria-expanded` on it.
7363 * @param {jQuery} $focusOwner
7365 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7366 this.$focusOwner
= $focusOwner
;
7370 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7371 * with an {@link OO.ui.mixin.IconElement icon} and/or
7372 * {@link OO.ui.mixin.IndicatorElement indicator}.
7373 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7374 * options. For more information about options and selects, please see the
7375 * [OOUI documentation on MediaWiki][1].
7378 * // Decorated options in a select widget.
7379 * var select = new OO.ui.SelectWidget( {
7381 * new OO.ui.DecoratedOptionWidget( {
7383 * label: 'Option with icon',
7386 * new OO.ui.DecoratedOptionWidget( {
7388 * label: 'Option with indicator',
7393 * $( document.body ).append( select.$element );
7395 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7398 * @extends OO.ui.OptionWidget
7399 * @mixins OO.ui.mixin.IconElement
7400 * @mixins OO.ui.mixin.IndicatorElement
7403 * @param {Object} [config] Configuration options
7405 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7406 // Parent constructor
7407 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7409 // Mixin constructors
7410 OO
.ui
.mixin
.IconElement
.call( this, config
);
7411 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7415 .addClass( 'oo-ui-decoratedOptionWidget' )
7416 .prepend( this.$icon
)
7417 .append( this.$indicator
);
7422 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7423 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7424 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7427 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7428 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7429 * the [OOUI documentation on MediaWiki] [1] for more information.
7431 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7434 * @extends OO.ui.DecoratedOptionWidget
7437 * @param {Object} [config] Configuration options
7439 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7440 // Parent constructor
7441 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7444 this.checkIcon
= new OO
.ui
.IconWidget( {
7446 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7451 .prepend( this.checkIcon
.$element
)
7452 .addClass( 'oo-ui-menuOptionWidget' );
7457 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7459 /* Static Properties */
7465 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7468 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to
7469 * group one or more related {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets
7470 * cannot be highlighted or selected.
7473 * var dropdown = new OO.ui.DropdownWidget( {
7476 * new OO.ui.MenuSectionOptionWidget( {
7479 * new OO.ui.MenuOptionWidget( {
7481 * label: 'Welsh Corgi'
7483 * new OO.ui.MenuOptionWidget( {
7485 * label: 'Standard Poodle'
7487 * new OO.ui.MenuSectionOptionWidget( {
7490 * new OO.ui.MenuOptionWidget( {
7497 * $( document.body ).append( dropdown.$element );
7500 * @extends OO.ui.DecoratedOptionWidget
7503 * @param {Object} [config] Configuration options
7505 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7506 // Parent constructor
7507 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7511 .addClass( 'oo-ui-menuSectionOptionWidget' )
7512 .removeAttr( 'role aria-selected' );
7513 this.selected
= false;
7518 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7520 /* Static Properties */
7526 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7532 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7535 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7536 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7537 * See {@link OO.ui.DropdownWidget DropdownWidget},
7538 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}, and
7539 * {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7540 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7541 * and customized to be opened, closed, and displayed as needed.
7543 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7544 * mouse outside the menu.
7546 * Menus also have support for keyboard interaction:
7548 * - Enter/Return key: choose and select a menu option
7549 * - Up-arrow key: highlight the previous menu option
7550 * - Down-arrow key: highlight the next menu option
7551 * - Escape key: hide the menu
7553 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7555 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7556 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7559 * @extends OO.ui.SelectWidget
7560 * @mixins OO.ui.mixin.ClippableElement
7561 * @mixins OO.ui.mixin.FloatableElement
7564 * @param {Object} [config] Configuration options
7565 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu
7566 * items that match the text the user types. This config is used by
7567 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget} and
7568 * {@link OO.ui.mixin.LookupElement LookupElement}
7569 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7570 * the text the user types. This config is used by
7571 * {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
7572 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks
7573 * the mouse anywhere on the page outside of this widget, the menu is hidden. For example, if
7574 * there is a button that toggles the menu's visibility on click, the menu will be hidden then
7575 * re-shown when the user clicks that button, unless the button (or its parent widget) is passed
7577 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7578 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7579 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7580 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7581 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7582 * @cfg {string} [filterMode='prefix'] The mode by which the menu filters the results.
7583 * Options are 'exact', 'prefix' or 'substring'. See `OO.ui.SelectWidget#getItemMatcher`
7584 * @param {number|string} [width] Width of the menu as a number of pixels or CSS string with unit
7585 * suffix, used by {@link OO.ui.mixin.ClippableElement ClippableElement}
7587 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7588 // Configuration initialization
7589 config
= config
|| {};
7591 // Parent constructor
7592 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7594 // Mixin constructors
7595 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( { $clippable
: this.$group
}, config
) );
7596 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7598 // Initial vertical positions other than 'center' will result in
7599 // the menu being flipped if there is not enough space in the container.
7600 // Store the original position so we know what to reset to.
7601 this.originalVerticalPosition
= this.verticalPosition
;
7604 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7605 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7606 this.filterFromInput
= !!config
.filterFromInput
;
7607 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7608 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7609 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7610 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7611 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7612 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7613 this.lastHighlightedItem
= null;
7614 this.width
= config
.width
;
7615 this.filterMode
= config
.filterMode
;
7618 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7619 if ( config
.widget
) {
7620 this.setFocusOwner( config
.widget
.$tabIndexed
);
7623 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7624 // that reference properties not initialized at that time of parent class construction
7625 // TODO: Find a better way to handle post-constructor setup
7626 this.visible
= false;
7627 this.$element
.addClass( 'oo-ui-element-hidden' );
7628 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7633 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7634 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7635 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7642 * The menu is ready: it is visible and has been positioned and clipped.
7645 /* Static properties */
7648 * Positions to flip to if there isn't room in the container for the
7649 * menu in a specific direction.
7651 * @property {Object.<string,string>}
7653 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7663 * Handles document mouse down events.
7666 * @param {MouseEvent} e Mouse down event
7668 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7672 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7677 this.toggle( false );
7684 OO
.ui
.MenuSelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
7685 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7687 if ( !this.isDisabled() && this.isVisible() ) {
7688 switch ( e
.keyCode
) {
7689 case OO
.ui
.Keys
.LEFT
:
7690 case OO
.ui
.Keys
.RIGHT
:
7691 // Do nothing if a text field is associated, arrow keys will be handled natively
7692 if ( !this.$input
) {
7693 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7696 case OO
.ui
.Keys
.ESCAPE
:
7697 case OO
.ui
.Keys
.TAB
:
7698 if ( currentItem
&& !this.multiselect
) {
7699 currentItem
.setHighlighted( false );
7701 this.toggle( false );
7702 // Don't prevent tabbing away, prevent defocusing
7703 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7705 e
.stopPropagation();
7709 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7716 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7717 * or after items were added/removed (always).
7721 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7722 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7724 len
= this.items
.length
,
7725 showAll
= !this.isVisible(),
7728 if ( this.$input
&& this.filterFromInput
) {
7729 filter
= showAll
? null : this.getItemMatcher( this.$input
.val(), this.filterMode
);
7730 exactFilter
= this.getItemMatcher( this.$input
.val(), 'exact' );
7731 // Hide non-matching options, and also hide section headers if all options
7732 // in their section are hidden.
7733 for ( i
= 0; i
< len
; i
++ ) {
7734 item
= this.items
[ i
];
7735 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7737 // If the previous section was empty, hide its header
7738 section
.toggle( showAll
|| !sectionEmpty
);
7741 sectionEmpty
= true;
7742 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7743 visible
= showAll
|| filter( item
);
7744 exactMatch
= exactMatch
|| exactFilter( item
);
7745 anyVisible
= anyVisible
|| visible
;
7746 sectionEmpty
= sectionEmpty
&& !visible
;
7747 item
.toggle( visible
);
7750 // Process the final section
7752 section
.toggle( showAll
|| !sectionEmpty
);
7755 if ( !anyVisible
) {
7756 this.highlightItem( null );
7759 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7762 this.highlightOnFilter
&&
7763 !( this.lastHighlightedItem
&& this.lastHighlightedItem
.isVisible() )
7765 // Highlight the first item on the list
7767 items
= this.getItems();
7768 for ( i
= 0; i
< items
.length
; i
++ ) {
7769 if ( items
[ i
].isVisible() ) {
7774 this.highlightItem( item
);
7775 this.lastHighlightedItem
= item
;
7780 // Reevaluate clipping
7787 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyDownListener = function () {
7788 if ( this.$input
) {
7789 this.$input
.on( 'keydown', this.onDocumentKeyDownHandler
);
7791 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyDownListener
.call( this );
7798 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
7799 if ( this.$input
) {
7800 this.$input
.off( 'keydown', this.onDocumentKeyDownHandler
);
7802 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyDownListener
.call( this );
7809 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7810 if ( this.$input
) {
7811 if ( this.filterFromInput
) {
7813 'keydown mouseup cut paste change input select',
7814 this.onInputEditHandler
7816 this.updateItemVisibility();
7819 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyPressListener
.call( this );
7826 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7827 if ( this.$input
) {
7828 if ( this.filterFromInput
) {
7830 'keydown mouseup cut paste change input select',
7831 this.onInputEditHandler
7833 this.updateItemVisibility();
7836 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyPressListener
.call( this );
7843 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is
7846 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with
7847 * the keyboard or mouse and it becomes selected. To select an item programmatically,
7848 * use the #selectItem method.
7850 * @param {OO.ui.OptionWidget} item Item to choose
7852 * @return {OO.ui.Widget} The widget, for chaining
7854 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7855 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7856 if ( this.hideOnChoose
) {
7857 this.toggle( false );
7865 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7867 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7869 this.updateItemVisibility();
7877 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7879 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7881 this.updateItemVisibility();
7889 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7891 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7893 this.updateItemVisibility();
7899 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7900 * `.toggle( true )` after its #$element is attached to the DOM.
7902 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7903 * it in the right place and with the right dimensions only work correctly while it is attached.
7904 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7905 * strictly enforced, so currently it only generates a warning in the browser console.
7910 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7911 var change
, originalHeight
, flippedHeight
, selectedItem
;
7913 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7914 change
= visible
!== this.isVisible();
7916 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7917 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7918 this.warnedUnattached
= true;
7921 if ( change
&& visible
) {
7922 // Reset position before showing the popup again. It's possible we no longer need to flip
7923 // (e.g. if the user scrolled).
7924 this.setVerticalPosition( this.originalVerticalPosition
);
7928 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7934 this.setIdealSize( this.width
);
7935 } else if ( this.$floatableContainer
) {
7936 this.$clippable
.css( 'width', 'auto' );
7938 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
7939 // Dropdown is smaller than handle so expand to width
7940 this.$floatableContainer
[ 0 ].offsetWidth
:
7941 // Dropdown is larger than handle so auto size
7944 this.$clippable
.css( 'width', '' );
7947 this.togglePositioning( !!this.$floatableContainer
);
7948 this.toggleClipping( true );
7950 this.bindDocumentKeyDownListener();
7951 this.bindDocumentKeyPressListener();
7954 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
7955 this.originalVerticalPosition
!== 'center'
7957 // If opening the menu in one direction causes it to be clipped, flip it
7958 originalHeight
= this.$element
.height();
7959 this.setVerticalPosition(
7960 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
7962 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
7963 // If flipping also causes it to be clipped, open in whichever direction
7964 // we have more space
7965 flippedHeight
= this.$element
.height();
7966 if ( originalHeight
> flippedHeight
) {
7967 this.setVerticalPosition( this.originalVerticalPosition
);
7971 // Note that we do not flip the menu's opening direction if the clipping changes
7972 // later (e.g. after the user scrolls), that seems like it would be annoying
7974 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7976 selectedItem
= this.findSelectedItem();
7977 if ( !this.multiselect
&& selectedItem
) {
7978 // TODO: Verify if this is even needed; This is already done on highlight changes
7979 // in SelectWidget#highlightItem, so we should just need to highlight the item we need to
7980 // highlight here and not bother with attr or checking selections.
7981 this.$focusOwner
.attr( 'aria-activedescendant', selectedItem
.getElementId() );
7982 selectedItem
.scrollElementIntoView( { duration
: 0 } );
7986 if ( this.autoHide
) {
7987 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7990 this.emit( 'ready' );
7992 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7993 this.unbindDocumentKeyDownListener();
7994 this.unbindDocumentKeyPressListener();
7995 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7996 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7997 this.togglePositioning( false );
7998 this.toggleClipping( false );
8006 * Scroll to the top of the menu
8008 OO
.ui
.MenuSelectWidget
.prototype.scrollToTop = function () {
8009 this.$element
.scrollTop( 0 );
8013 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
8014 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
8015 * users can interact with it.
8017 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8018 * OO.ui.DropdownInputWidget instead.
8021 * // A DropdownWidget with a menu that contains three options.
8022 * var dropDown = new OO.ui.DropdownWidget( {
8023 * label: 'Dropdown menu: Select a menu option',
8026 * new OO.ui.MenuOptionWidget( {
8030 * new OO.ui.MenuOptionWidget( {
8034 * new OO.ui.MenuOptionWidget( {
8042 * $( document.body ).append( dropDown.$element );
8044 * dropDown.getMenu().selectItemByData( 'b' );
8046 * dropDown.getMenu().findSelectedItem().getData(); // Returns 'b'.
8048 * For more information, please see the [OOUI documentation on MediaWiki] [1].
8050 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8053 * @extends OO.ui.Widget
8054 * @mixins OO.ui.mixin.IconElement
8055 * @mixins OO.ui.mixin.IndicatorElement
8056 * @mixins OO.ui.mixin.LabelElement
8057 * @mixins OO.ui.mixin.TitledElement
8058 * @mixins OO.ui.mixin.TabIndexedElement
8061 * @param {Object} [config] Configuration options
8062 * @cfg {Object} [menu] Configuration options to pass to
8063 * {@link OO.ui.MenuSelectWidget menu select widget}.
8064 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
8065 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
8066 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
8067 * uses relative positioning.
8068 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
8070 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
8071 // Configuration initialization
8072 config
= $.extend( { indicator
: 'down' }, config
);
8074 // Parent constructor
8075 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
8077 // Properties (must be set before TabIndexedElement constructor call)
8078 this.$handle
= $( '<button>' );
8079 this.$overlay
= ( config
.$overlay
=== true ?
8080 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
8082 // Mixin constructors
8083 OO
.ui
.mixin
.IconElement
.call( this, config
);
8084 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8085 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8086 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
8087 $titled
: this.$label
8089 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
8090 $tabIndexed
: this.$handle
8094 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
8096 $floatableContainer
: this.$element
8101 click
: this.onClick
.bind( this ),
8102 keydown
: this.onKeyDown
.bind( this ),
8103 // Hack? Handle type-to-search when menu is not expanded and not handling its own events.
8104 keypress
: this.menu
.onDocumentKeyPressHandler
,
8105 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
8107 this.menu
.connect( this, {
8108 select
: 'onMenuSelect',
8109 toggle
: 'onMenuToggle'
8114 .addClass( 'oo-ui-dropdownWidget-handle' )
8117 'aria-owns': this.menu
.getElementId(),
8118 'aria-haspopup': 'listbox'
8120 .append( this.$icon
, this.$label
, this.$indicator
);
8122 .addClass( 'oo-ui-dropdownWidget' )
8123 .append( this.$handle
);
8124 this.$overlay
.append( this.menu
.$element
);
8129 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8130 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8131 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8132 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8133 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8134 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8141 * @return {OO.ui.MenuSelectWidget} Menu of widget
8143 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8148 * Handles menu select events.
8151 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8153 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8157 this.setLabel( null );
8161 selectedLabel
= item
.getLabel();
8163 // If the label is a DOM element, clone it, because setLabel will append() it
8164 if ( selectedLabel
instanceof $ ) {
8165 selectedLabel
= selectedLabel
.clone();
8168 this.setLabel( selectedLabel
);
8172 * Handle menu toggle events.
8175 * @param {boolean} isVisible Open state of the menu
8177 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8178 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8182 * Handle mouse click events.
8185 * @param {jQuery.Event} e Mouse click event
8186 * @return {undefined/boolean} False to prevent default if event is handled
8188 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8189 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8196 * Handle key down events.
8199 * @param {jQuery.Event} e Key down event
8200 * @return {undefined/boolean} False to prevent default if event is handled
8202 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8204 !this.isDisabled() &&
8206 e
.which
=== OO
.ui
.Keys
.ENTER
||
8208 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8209 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8210 // Space only closes the menu is the user is not typing to search.
8211 this.menu
.keyPressBuffer
=== ''
8214 !this.menu
.isVisible() &&
8216 e
.which
=== OO
.ui
.Keys
.UP
||
8217 e
.which
=== OO
.ui
.Keys
.DOWN
8228 * RadioOptionWidget is an option widget that looks like a radio button.
8229 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8230 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8232 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8235 * @extends OO.ui.OptionWidget
8238 * @param {Object} [config] Configuration options
8240 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8241 // Configuration initialization
8242 config
= config
|| {};
8244 // Properties (must be done before parent constructor which calls #setDisabled)
8245 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8247 // Parent constructor
8248 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8251 // Remove implicit role, we're handling it ourselves
8252 this.radio
.$input
.attr( 'role', 'presentation' );
8254 .addClass( 'oo-ui-radioOptionWidget' )
8255 .attr( 'role', 'radio' )
8256 .attr( 'aria-checked', 'false' )
8257 .removeAttr( 'aria-selected' )
8258 .prepend( this.radio
.$element
);
8263 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8265 /* Static Properties */
8271 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8277 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8283 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8289 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8296 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8297 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8299 this.radio
.setSelected( state
);
8301 .attr( 'aria-checked', state
.toString() )
8302 .removeAttr( 'aria-selected' );
8310 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8311 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8313 this.radio
.setDisabled( this.isDisabled() );
8319 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8320 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8321 * an interface for adding, removing and selecting options.
8322 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8324 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8325 * OO.ui.RadioSelectInputWidget instead.
8328 * // A RadioSelectWidget with RadioOptions.
8329 * var option1 = new OO.ui.RadioOptionWidget( {
8331 * label: 'Selected radio option'
8333 * option2 = new OO.ui.RadioOptionWidget( {
8335 * label: 'Unselected radio option'
8337 * radioSelect = new OO.ui.RadioSelectWidget( {
8338 * items: [ option1, option2 ]
8341 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8342 * radioSelect.selectItem( option1 );
8344 * $( document.body ).append( radioSelect.$element );
8346 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8350 * @extends OO.ui.SelectWidget
8351 * @mixins OO.ui.mixin.TabIndexedElement
8354 * @param {Object} [config] Configuration options
8356 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8357 // Parent constructor
8358 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8360 // Mixin constructors
8361 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8365 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8366 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8371 .addClass( 'oo-ui-radioSelectWidget' )
8372 .attr( 'role', 'radiogroup' );
8377 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8378 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8381 * MultioptionWidgets are special elements that can be selected and configured with data. The
8382 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8383 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8384 * and examples, please see the [OOUI documentation on MediaWiki][1].
8386 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8389 * @extends OO.ui.Widget
8390 * @mixins OO.ui.mixin.ItemWidget
8391 * @mixins OO.ui.mixin.LabelElement
8392 * @mixins OO.ui.mixin.TitledElement
8395 * @param {Object} [config] Configuration options
8396 * @cfg {boolean} [selected=false] Whether the option is initially selected
8398 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8399 // Configuration initialization
8400 config
= config
|| {};
8402 // Parent constructor
8403 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8405 // Mixin constructors
8406 OO
.ui
.mixin
.ItemWidget
.call( this );
8407 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8408 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8411 this.selected
= null;
8415 .addClass( 'oo-ui-multioptionWidget' )
8416 .append( this.$label
);
8417 this.setSelected( config
.selected
);
8422 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8423 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8424 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8425 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.TitledElement
);
8432 * A change event is emitted when the selected state of the option changes.
8434 * @param {boolean} selected Whether the option is now selected
8440 * Check if the option is selected.
8442 * @return {boolean} Item is selected
8444 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8445 return this.selected
;
8449 * Set the option’s selected state. In general, all modifications to the selection
8450 * should be handled by the SelectWidget’s
8451 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
8453 * @param {boolean} [state=false] Select option
8455 * @return {OO.ui.Widget} The widget, for chaining
8457 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8459 if ( this.selected
!== state
) {
8460 this.selected
= state
;
8461 this.emit( 'change', state
);
8462 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8468 * MultiselectWidget allows selecting multiple options from a list.
8470 * For more information about menus and options, please see the [OOUI documentation
8473 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8477 * @extends OO.ui.Widget
8478 * @mixins OO.ui.mixin.GroupWidget
8479 * @mixins OO.ui.mixin.TitledElement
8482 * @param {Object} [config] Configuration options
8483 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8485 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8486 // Parent constructor
8487 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8489 // Configuration initialization
8490 config
= config
|| {};
8492 // Mixin constructors
8493 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8494 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8500 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8501 // by GroupElement only when items are added/removed
8502 this.connect( this, {
8503 select
: [ 'emit', 'change' ]
8507 if ( config
.items
) {
8508 this.addItems( config
.items
);
8510 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8511 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8512 .append( this.$group
);
8517 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8518 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8519 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.TitledElement
);
8526 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8532 * A select event is emitted when an item is selected or deselected.
8538 * Find options that are selected.
8540 * @return {OO.ui.MultioptionWidget[]} Selected options
8542 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8543 return this.items
.filter( function ( item
) {
8544 return item
.isSelected();
8549 * Find the data of options that are selected.
8551 * @return {Object[]|string[]} Values of selected options
8553 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8554 return this.findSelectedItems().map( function ( item
) {
8560 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8562 * @param {OO.ui.MultioptionWidget[]} items Items to select
8564 * @return {OO.ui.Widget} The widget, for chaining
8566 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8567 this.items
.forEach( function ( item
) {
8568 var selected
= items
.indexOf( item
) !== -1;
8569 item
.setSelected( selected
);
8575 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8577 * @param {Object[]|string[]} datas Values of items to select
8579 * @return {OO.ui.Widget} The widget, for chaining
8581 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8584 items
= datas
.map( function ( data
) {
8585 return widget
.findItemFromData( data
);
8587 this.selectItems( items
);
8592 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8593 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8594 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8596 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8599 * @extends OO.ui.MultioptionWidget
8602 * @param {Object} [config] Configuration options
8604 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8605 // Configuration initialization
8606 config
= config
|| {};
8608 // Properties (must be done before parent constructor which calls #setDisabled)
8609 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8611 // Parent constructor
8612 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8615 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8616 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8620 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8621 .prepend( this.checkbox
.$element
);
8626 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8628 /* Static Properties */
8634 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8639 * Handle checkbox selected state change.
8643 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8644 this.setSelected( this.checkbox
.isSelected() );
8650 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8651 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8652 this.checkbox
.setSelected( state
);
8659 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8660 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8661 this.checkbox
.setDisabled( this.isDisabled() );
8668 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8669 this.checkbox
.focus();
8673 * Handle key down events.
8676 * @param {jQuery.Event} e
8678 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8680 element
= this.getElementGroup(),
8683 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8684 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8685 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8686 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8696 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8697 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8698 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8699 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8701 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8702 * OO.ui.CheckboxMultiselectInputWidget instead.
8705 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8706 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8709 * label: 'Selected checkbox'
8711 * option2 = new OO.ui.CheckboxMultioptionWidget( {
8713 * label: 'Unselected checkbox'
8715 * multiselect = new OO.ui.CheckboxMultiselectWidget( {
8716 * items: [ option1, option2 ]
8718 * $( document.body ).append( multiselect.$element );
8720 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8723 * @extends OO.ui.MultiselectWidget
8726 * @param {Object} [config] Configuration options
8728 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8729 // Parent constructor
8730 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8733 this.$lastClicked
= null;
8736 this.$group
.on( 'click', this.onClick
.bind( this ) );
8739 this.$element
.addClass( 'oo-ui-checkboxMultiselectWidget' );
8744 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8749 * Get an option by its position relative to the specified item (or to the start of the
8750 * option array, if item is `null`). The direction in which to search through the option array
8751 * is specified with a number: -1 for reverse (the default) or 1 for forward. The method will
8752 * return an option, or `null` if there are no options in the array.
8754 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or
8755 * `null` to start at the beginning of the array.
8756 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8757 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items
8760 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8761 var currentIndex
, nextIndex
, i
,
8762 increase
= direction
> 0 ? 1 : -1,
8763 len
= this.items
.length
;
8766 currentIndex
= this.items
.indexOf( item
);
8767 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8769 // If no item is selected and moving forward, start at the beginning.
8770 // If moving backward, start at the end.
8771 nextIndex
= direction
> 0 ? 0 : len
- 1;
8774 for ( i
= 0; i
< len
; i
++ ) {
8775 item
= this.items
[ nextIndex
];
8776 if ( item
&& !item
.isDisabled() ) {
8779 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8785 * Handle click events on checkboxes.
8787 * @param {jQuery.Event} e
8789 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8790 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8791 $lastClicked
= this.$lastClicked
,
8792 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8793 .not( '.oo-ui-widget-disabled' );
8795 // Allow selecting multiple options at once by Shift-clicking them
8796 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8797 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8798 lastClickedIndex
= $options
.index( $lastClicked
);
8799 nowClickedIndex
= $options
.index( $nowClicked
);
8800 // If it's the same item, either the user is being silly, or it's a fake event generated
8801 // by the browser. In either case we don't need custom handling.
8802 if ( nowClickedIndex
!== lastClickedIndex
) {
8804 wasSelected
= items
[ nowClickedIndex
].isSelected();
8805 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8807 // This depends on the DOM order of the items and the order of the .items array being
8809 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8810 if ( !items
[ i
].isDisabled() ) {
8811 items
[ i
].setSelected( !wasSelected
);
8814 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8815 // handling first, then set our value. The order in which events happen is different for
8816 // clicks on the <input> and on the <label> and there are additional fake clicks fired
8817 // for non-click actions that change the checkboxes.
8819 setTimeout( function () {
8820 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8821 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8827 if ( $nowClicked
.length
) {
8828 this.$lastClicked
= $nowClicked
;
8836 * @return {OO.ui.Widget} The widget, for chaining
8838 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8840 if ( !this.isDisabled() ) {
8841 item
= this.getRelativeFocusableItem( null, 1 );
8852 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8857 * Progress bars visually display the status of an operation, such as a download,
8858 * and can be either determinate or indeterminate:
8860 * - **determinate** process bars show the percent of an operation that is complete.
8862 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8863 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8864 * not use percentages.
8866 * The value of the `progress` configuration determines whether the bar is determinate
8870 * // Examples of determinate and indeterminate progress bars.
8871 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8874 * var progressBar2 = new OO.ui.ProgressBarWidget();
8876 * // Create a FieldsetLayout to layout progress bars.
8877 * var fieldset = new OO.ui.FieldsetLayout;
8878 * fieldset.addItems( [
8879 * new OO.ui.FieldLayout( progressBar1, {
8880 * label: 'Determinate',
8883 * new OO.ui.FieldLayout( progressBar2, {
8884 * label: 'Indeterminate',
8888 * $( document.body ).append( fieldset.$element );
8891 * @extends OO.ui.Widget
8894 * @param {Object} [config] Configuration options
8895 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8896 * To create a determinate progress bar, specify a number that reflects the initial
8898 * By default, the progress bar is indeterminate.
8900 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8901 // Configuration initialization
8902 config
= config
|| {};
8904 // Parent constructor
8905 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8908 this.$bar
= $( '<div>' );
8909 this.progress
= null;
8912 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8913 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8916 role
: 'progressbar',
8918 'aria-valuemax': 100
8920 .addClass( 'oo-ui-progressBarWidget' )
8921 .append( this.$bar
);
8926 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8928 /* Static Properties */
8934 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8939 * Get the percent of the progress that has been completed. Indeterminate progresses will
8942 * @return {number|boolean} Progress percent
8944 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8945 return this.progress
;
8949 * Set the percent of the process completed or `false` for an indeterminate process.
8951 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8953 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8954 this.progress
= progress
;
8956 if ( progress
!== false ) {
8957 this.$bar
.css( 'width', this.progress
+ '%' );
8958 this.$element
.attr( 'aria-valuenow', this.progress
);
8960 this.$bar
.css( 'width', '' );
8961 this.$element
.removeAttr( 'aria-valuenow' );
8963 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8967 * InputWidget is the base class for all input widgets, which
8968 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox
8969 * inputs}, {@link OO.ui.RadioInputWidget radio inputs}, and
8970 * {@link OO.ui.ButtonInputWidget button inputs}.
8971 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
8973 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
8977 * @extends OO.ui.Widget
8978 * @mixins OO.ui.mixin.TabIndexedElement
8979 * @mixins OO.ui.mixin.TitledElement
8980 * @mixins OO.ui.mixin.AccessKeyedElement
8983 * @param {Object} [config] Configuration options
8984 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8985 * @cfg {string} [value=''] The value of the input.
8986 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8987 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8988 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the
8989 * value of an input before it is accepted.
8991 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8992 // Configuration initialization
8993 config
= config
|| {};
8995 // Parent constructor
8996 OO
.ui
.InputWidget
.parent
.call( this, config
);
8999 // See #reusePreInfuseDOM about config.$input
9000 this.$input
= config
.$input
|| this.getInputElement( config
);
9002 this.inputFilter
= config
.inputFilter
;
9004 // Mixin constructors
9005 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
9006 $tabIndexed
: this.$input
9008 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
9009 $titled
: this.$input
9011 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
9012 $accessKeyed
: this.$input
9016 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
9020 .addClass( 'oo-ui-inputWidget-input' )
9021 .attr( 'name', config
.name
)
9022 .prop( 'disabled', this.isDisabled() );
9024 .addClass( 'oo-ui-inputWidget' )
9025 .append( this.$input
);
9026 this.setValue( config
.value
);
9028 this.setDir( config
.dir
);
9030 if ( config
.inputId
!== undefined ) {
9031 this.setInputId( config
.inputId
);
9037 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
9038 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
9039 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
9040 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
9042 /* Static Methods */
9047 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9048 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9049 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
9050 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
9057 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9058 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9059 if ( config
.$input
&& config
.$input
.length
) {
9060 state
.value
= config
.$input
.val();
9061 // Might be better in TabIndexedElement, but it's awkward to do there because
9062 // mixins are awkward
9063 state
.focus
= config
.$input
.is( ':focus' );
9073 * A change event is emitted when the value of the input changes.
9075 * @param {string} value
9081 * Get input element.
9083 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9084 * different circumstances. The element must have a `value` property (like form elements).
9087 * @param {Object} config Configuration options
9088 * @return {jQuery} Input element
9090 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
9091 return $( '<input>' );
9095 * Handle potentially value-changing events.
9098 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9100 OO
.ui
.InputWidget
.prototype.onEdit = function () {
9102 if ( !this.isDisabled() ) {
9103 // Allow the stack to clear so the value will be updated
9104 setTimeout( function () {
9105 widget
.setValue( widget
.$input
.val() );
9111 * Get the value of the input.
9113 * @return {string} Input value
9115 OO
.ui
.InputWidget
.prototype.getValue = function () {
9116 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9117 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9118 var value
= this.$input
.val();
9119 if ( this.value
!== value
) {
9120 this.setValue( value
);
9126 * Set the directionality of the input.
9128 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9130 * @return {OO.ui.Widget} The widget, for chaining
9132 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9133 this.$input
.prop( 'dir', dir
);
9138 * Set the value of the input.
9140 * @param {string} value New value
9143 * @return {OO.ui.Widget} The widget, for chaining
9145 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9146 value
= this.cleanUpValue( value
);
9147 // Update the DOM if it has changed. Note that with cleanUpValue, it
9148 // is possible for the DOM value to change without this.value changing.
9149 if ( this.$input
.val() !== value
) {
9150 this.$input
.val( value
);
9152 if ( this.value
!== value
) {
9154 this.emit( 'change', this.value
);
9156 // The first time that the value is set (probably while constructing the widget),
9157 // remember it in defaultValue. This property can be later used to check whether
9158 // the value of the input has been changed since it was created.
9159 if ( this.defaultValue
=== undefined ) {
9160 this.defaultValue
= this.value
;
9161 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9167 * Clean up incoming value.
9169 * Ensures value is a string, and converts undefined and null to empty string.
9172 * @param {string} value Original value
9173 * @return {string} Cleaned up value
9175 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9176 if ( value
=== undefined || value
=== null ) {
9178 } else if ( this.inputFilter
) {
9179 return this.inputFilter( String( value
) );
9181 return String( value
);
9188 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9189 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9190 if ( this.$input
) {
9191 this.$input
.prop( 'disabled', this.isDisabled() );
9197 * Set the 'id' attribute of the `<input>` element.
9199 * @param {string} id
9201 * @return {OO.ui.Widget} The widget, for chaining
9203 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9204 this.$input
.attr( 'id', id
);
9211 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9212 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9213 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9214 this.setValue( state
.value
);
9216 if ( state
.focus
) {
9222 * Data widget intended for creating `<input type="hidden">` inputs.
9225 * @extends OO.ui.Widget
9228 * @param {Object} [config] Configuration options
9229 * @cfg {string} [value=''] The value of the input.
9230 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9232 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9233 // Configuration initialization
9234 config
= $.extend( { value
: '', name
: '' }, config
);
9236 // Parent constructor
9237 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9240 this.$element
.attr( {
9242 value
: config
.value
,
9245 this.$element
.removeAttr( 'aria-disabled' );
9250 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9252 /* Static Properties */
9258 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9261 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9262 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9263 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9264 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9265 * [OOUI documentation on MediaWiki] [1] for more information.
9268 * // A ButtonInputWidget rendered as an HTML button, the default.
9269 * var button = new OO.ui.ButtonInputWidget( {
9270 * label: 'Input button',
9274 * $( document.body ).append( button.$element );
9276 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9279 * @extends OO.ui.InputWidget
9280 * @mixins OO.ui.mixin.ButtonElement
9281 * @mixins OO.ui.mixin.IconElement
9282 * @mixins OO.ui.mixin.IndicatorElement
9283 * @mixins OO.ui.mixin.LabelElement
9284 * @mixins OO.ui.mixin.FlaggedElement
9287 * @param {Object} [config] Configuration options
9288 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute:
9289 * 'button', 'submit' or 'reset'.
9290 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9291 * Widgets configured to be an `<input>` do not support {@link #icon icons} and
9292 * {@link #indicator indicators},
9293 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should
9294 * only be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9296 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9297 // Configuration initialization
9298 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9300 // See InputWidget#reusePreInfuseDOM about config.$input
9301 if ( config
.$input
) {
9302 config
.$input
.empty();
9305 // Properties (must be set before parent constructor, which calls #setValue)
9306 this.useInputTag
= config
.useInputTag
;
9308 // Parent constructor
9309 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9311 // Mixin constructors
9312 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {
9313 $button
: this.$input
9315 OO
.ui
.mixin
.IconElement
.call( this, config
);
9316 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9317 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9318 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
9321 if ( !config
.useInputTag
) {
9322 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9324 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9329 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9330 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9331 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9332 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9333 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9334 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
9336 /* Static Properties */
9342 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9350 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9352 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9353 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9359 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9361 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9362 * text, or `null` for no label
9364 * @return {OO.ui.Widget} The widget, for chaining
9366 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9367 if ( typeof label
=== 'function' ) {
9368 label
= OO
.ui
.resolveMsg( label
);
9371 if ( this.useInputTag
) {
9372 // Discard non-plaintext labels
9373 if ( typeof label
!== 'string' ) {
9377 this.$input
.val( label
);
9380 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9384 * Set the value of the input.
9386 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9387 * they do not support {@link #value values}.
9389 * @param {string} value New value
9391 * @return {OO.ui.Widget} The widget, for chaining
9393 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9394 if ( !this.useInputTag
) {
9395 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9403 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9404 // Disable generating `<label>` elements for buttons. One would very rarely need additional
9405 // label for a button, and it's already a big clickable target, and it causes
9406 // unexpected rendering.
9411 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9412 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9413 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9414 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9416 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9419 * // An example of selected, unselected, and disabled checkbox inputs.
9420 * var checkbox1 = new OO.ui.CheckboxInputWidget( {
9424 * checkbox2 = new OO.ui.CheckboxInputWidget( {
9427 * checkbox3 = new OO.ui.CheckboxInputWidget( {
9431 * // Create a fieldset layout with fields for each checkbox.
9432 * fieldset = new OO.ui.FieldsetLayout( {
9433 * label: 'Checkboxes'
9435 * fieldset.addItems( [
9436 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9437 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9438 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9440 * $( document.body ).append( fieldset.$element );
9442 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9445 * @extends OO.ui.InputWidget
9448 * @param {Object} [config] Configuration options
9449 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is
9451 * @cfg {boolean} [indeterminate=false] Whether the checkbox is in the indeterminate state.
9453 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9454 // Configuration initialization
9455 config
= config
|| {};
9457 // Parent constructor
9458 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9461 this.checkIcon
= new OO
.ui
.IconWidget( {
9463 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9468 .addClass( 'oo-ui-checkboxInputWidget' )
9469 // Required for pretty styling in WikimediaUI theme
9470 .append( this.checkIcon
.$element
);
9471 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9472 this.setIndeterminate( config
.indeterminate
!== undefined ? config
.indeterminate
: false );
9477 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9484 * A change event is emitted when the state of the input changes.
9486 * @param {boolean} selected
9487 * @param {boolean} indeterminate
9490 /* Static Properties */
9496 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9498 /* Static Methods */
9503 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9504 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9505 state
.checked
= config
.$input
.prop( 'checked' );
9515 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9516 return $( '<input>' ).attr( 'type', 'checkbox' );
9522 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9524 if ( !this.isDisabled() ) {
9525 // Allow the stack to clear so the value will be updated
9526 setTimeout( function () {
9527 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9528 widget
.setIndeterminate( widget
.$input
.prop( 'indeterminate' ) );
9534 * Set selection state of this checkbox.
9536 * @param {boolean} state Selected state
9537 * @param {boolean} internal Used for internal calls to suppress events
9539 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9541 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
, internal ) {
9543 if ( this.selected
!== state
) {
9544 this.selected
= state
;
9545 this.$input
.prop( 'checked', this.selected
);
9547 this.setIndeterminate( false, true );
9548 this.emit( 'change', this.selected
, this.indeterminate
);
9551 // The first time that the selection state is set (probably while constructing the widget),
9552 // remember it in defaultSelected. This property can be later used to check whether
9553 // the selection state of the input has been changed since it was created.
9554 if ( this.defaultSelected
=== undefined ) {
9555 this.defaultSelected
= this.selected
;
9556 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9562 * Check if this checkbox is selected.
9564 * @return {boolean} Checkbox is selected
9566 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9567 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9568 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9569 var selected
= this.$input
.prop( 'checked' );
9570 if ( this.selected
!== selected
) {
9571 this.setSelected( selected
);
9573 return this.selected
;
9577 * Set indeterminate state of this checkbox.
9579 * @param {boolean} state Indeterminate state
9580 * @param {boolean} internal Used for internal calls to suppress events
9582 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9584 OO
.ui
.CheckboxInputWidget
.prototype.setIndeterminate = function ( state
, internal ) {
9586 if ( this.indeterminate
!== state
) {
9587 this.indeterminate
= state
;
9588 this.$input
.prop( 'indeterminate', this.indeterminate
);
9590 this.setSelected( false, true );
9591 this.emit( 'change', this.selected
, this.indeterminate
);
9598 * Check if this checkbox is selected.
9600 * @return {boolean} Checkbox is selected
9602 OO
.ui
.CheckboxInputWidget
.prototype.isIndeterminate = function () {
9603 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9604 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9605 var indeterminate
= this.$input
.prop( 'indeterminate' );
9606 if ( this.indeterminate
!== indeterminate
) {
9607 this.setIndeterminate( indeterminate
);
9609 return this.indeterminate
;
9615 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9616 if ( !this.isDisabled() ) {
9617 this.$handle
.trigger( 'click' );
9625 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9626 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9627 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9628 this.setSelected( state
.checked
);
9633 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9634 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the
9635 * value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9636 * more information about input widgets.
9638 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9639 * are no options. If no `value` configuration option is provided, the first option is selected.
9640 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9642 * This and OO.ui.RadioSelectInputWidget support similar configuration options.
9645 * // A DropdownInputWidget with three options.
9646 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9648 * { data: 'a', label: 'First' },
9649 * { data: 'b', label: 'Second', disabled: true },
9650 * { optgroup: 'Group label' },
9651 * { data: 'c', label: 'First sub-item)' }
9654 * $( document.body ).append( dropdownInput.$element );
9656 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9659 * @extends OO.ui.InputWidget
9662 * @param {Object} [config] Configuration options
9663 * @cfg {Object[]} [options=[]] Array of menu options in the format described above.
9664 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9665 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
9666 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
9667 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
9668 * uses relative positioning.
9669 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9671 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9672 // Configuration initialization
9673 config
= config
|| {};
9675 // Properties (must be done before parent constructor which calls #setDisabled)
9676 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9678 $overlay
: config
.$overlay
9682 // Set up the options before parent constructor, which uses them to validate config.value.
9683 // Use this instead of setOptions() because this.$input is not set up yet.
9684 this.setOptionsData( config
.options
|| [] );
9686 // Parent constructor
9687 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9690 this.dropdownWidget
.getMenu().connect( this, {
9691 select
: 'onMenuSelect'
9696 .addClass( 'oo-ui-dropdownInputWidget' )
9697 .append( this.dropdownWidget
.$element
);
9698 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9699 this.setTitledElement( this.dropdownWidget
.$handle
);
9704 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9712 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9713 return $( '<select>' );
9717 * Handles menu select events.
9720 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9722 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9723 this.setValue( item
? item
.getData() : '' );
9729 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9731 value
= this.cleanUpValue( value
);
9732 // Only allow setting values that are actually present in the dropdown
9733 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9734 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9735 this.dropdownWidget
.getMenu().selectItem( selected
);
9736 value
= selected
? selected
.getData() : '';
9737 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9738 if ( this.optionsDirty
) {
9739 // We reached this from the constructor or from #setOptions.
9740 // We have to update the <select> element.
9741 this.updateOptionsInterface();
9749 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9750 this.dropdownWidget
.setDisabled( state
);
9751 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9756 * Set the options available for this input.
9758 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9760 * @return {OO.ui.Widget} The widget, for chaining
9762 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9763 var value
= this.getValue();
9765 this.setOptionsData( options
);
9767 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9768 // In case the previous value is no longer an available option, select the first valid one.
9769 this.setValue( value
);
9775 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9777 * This method may be called before the parent constructor, so various properties may not be
9780 * @param {Object[]} options Array of menu options (see #constructor for details).
9783 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9784 var optionWidgets
, optIndex
, opt
, previousOptgroup
, optionWidget
, optValue
,
9787 this.optionsDirty
= true;
9789 // Go through all the supplied option configs and create either
9790 // MenuSectionOption or MenuOption widgets from each.
9792 for ( optIndex
= 0; optIndex
< options
.length
; optIndex
++ ) {
9793 opt
= options
[ optIndex
];
9795 if ( opt
.optgroup
!== undefined ) {
9796 // Create a <optgroup> menu item.
9797 optionWidget
= widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9798 previousOptgroup
= optionWidget
;
9801 // Create a normal <option> menu item.
9802 optValue
= widget
.cleanUpValue( opt
.data
);
9803 optionWidget
= widget
.createMenuOptionWidget(
9805 opt
.label
!== undefined ? opt
.label
: optValue
9809 // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
9811 opt
.disabled
!== undefined ||
9812 previousOptgroup
instanceof OO
.ui
.MenuSectionOptionWidget
&&
9813 previousOptgroup
.isDisabled()
9815 optionWidget
.setDisabled( true );
9818 optionWidgets
.push( optionWidget
);
9821 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9825 * Create a menu option widget.
9828 * @param {string} data Item data
9829 * @param {string} label Item label
9830 * @return {OO.ui.MenuOptionWidget} Option widget
9832 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9833 return new OO
.ui
.MenuOptionWidget( {
9840 * Create a menu section option widget.
9843 * @param {string} label Section item label
9844 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9846 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9847 return new OO
.ui
.MenuSectionOptionWidget( {
9853 * Update the user-visible interface to match the internal list of options and value.
9855 * This method must only be called after the parent constructor.
9859 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9861 $optionsContainer
= this.$input
,
9862 defaultValue
= this.defaultValue
,
9865 this.$input
.empty();
9867 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9870 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9871 $optionNode
= $( '<option>' )
9872 .attr( 'value', optionWidget
.getData() )
9873 .text( optionWidget
.getLabel() );
9875 // Remember original selection state. This property can be later used to check whether
9876 // the selection state of the input has been changed since it was created.
9877 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9879 $optionsContainer
.append( $optionNode
);
9881 $optionNode
= $( '<optgroup>' )
9882 .attr( 'label', optionWidget
.getLabel() );
9883 widget
.$input
.append( $optionNode
);
9884 $optionsContainer
= $optionNode
;
9887 // Disable the option or optgroup if required.
9888 if ( optionWidget
.isDisabled() ) {
9889 $optionNode
.prop( 'disabled', true );
9893 this.optionsDirty
= false;
9899 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9900 this.dropdownWidget
.focus();
9907 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9908 this.dropdownWidget
.blur();
9913 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9914 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9915 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9916 * please see the [OOUI documentation on MediaWiki][1].
9918 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9921 * // An example of selected, unselected, and disabled radio inputs
9922 * var radio1 = new OO.ui.RadioInputWidget( {
9926 * var radio2 = new OO.ui.RadioInputWidget( {
9929 * var radio3 = new OO.ui.RadioInputWidget( {
9933 * // Create a fieldset layout with fields for each radio button.
9934 * var fieldset = new OO.ui.FieldsetLayout( {
9935 * label: 'Radio inputs'
9937 * fieldset.addItems( [
9938 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9939 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9940 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9942 * $( document.body ).append( fieldset.$element );
9944 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9947 * @extends OO.ui.InputWidget
9950 * @param {Object} [config] Configuration options
9951 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button
9954 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9955 // Configuration initialization
9956 config
= config
|| {};
9958 // Parent constructor
9959 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9963 .addClass( 'oo-ui-radioInputWidget' )
9964 // Required for pretty styling in WikimediaUI theme
9965 .append( $( '<span>' ) );
9966 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9971 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9973 /* Static Properties */
9979 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9981 /* Static Methods */
9986 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9987 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9988 state
.checked
= config
.$input
.prop( 'checked' );
9998 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9999 return $( '<input>' ).attr( 'type', 'radio' );
10005 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
10006 // RadioInputWidget doesn't track its state.
10010 * Set selection state of this radio button.
10012 * @param {boolean} state `true` for selected
10014 * @return {OO.ui.Widget} The widget, for chaining
10016 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
10017 // RadioInputWidget doesn't track its state.
10018 this.$input
.prop( 'checked', state
);
10019 // The first time that the selection state is set (probably while constructing the widget),
10020 // remember it in defaultSelected. This property can be later used to check whether
10021 // the selection state of the input has been changed since it was created.
10022 if ( this.defaultSelected
=== undefined ) {
10023 this.defaultSelected
= state
;
10024 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
10030 * Check if this radio button is selected.
10032 * @return {boolean} Radio is selected
10034 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
10035 return this.$input
.prop( 'checked' );
10041 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
10042 if ( !this.isDisabled() ) {
10043 this.$input
.trigger( 'click' );
10051 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10052 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10053 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
10054 this.setSelected( state
.checked
);
10059 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be
10060 * used within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with
10061 * the value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
10062 * more information about input widgets.
10064 * This and OO.ui.DropdownInputWidget support similar configuration options.
10067 * // A RadioSelectInputWidget with three options
10068 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
10070 * { data: 'a', label: 'First' },
10071 * { data: 'b', label: 'Second'},
10072 * { data: 'c', label: 'Third' }
10075 * $( document.body ).append( radioSelectInput.$element );
10077 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10080 * @extends OO.ui.InputWidget
10083 * @param {Object} [config] Configuration options
10084 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10086 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
10087 // Configuration initialization
10088 config
= config
|| {};
10090 // Properties (must be done before parent constructor which calls #setDisabled)
10091 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
10092 // Set up the options before parent constructor, which uses them to validate config.value.
10093 // Use this instead of setOptions() because this.$input is not set up yet
10094 this.setOptionsData( config
.options
|| [] );
10096 // Parent constructor
10097 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
10100 this.radioSelectWidget
.connect( this, {
10101 select
: 'onMenuSelect'
10106 .addClass( 'oo-ui-radioSelectInputWidget' )
10107 .append( this.radioSelectWidget
.$element
);
10108 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
10113 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
10115 /* Static Methods */
10120 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10121 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10122 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
10129 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10130 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10131 // Cannot reuse the `<input type=radio>` set
10132 delete config
.$input
;
10142 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
10143 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
10144 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
10145 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
10149 * Handles menu select events.
10152 * @param {OO.ui.RadioOptionWidget} item Selected menu item
10154 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
10155 this.setValue( item
.getData() );
10161 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
10163 value
= this.cleanUpValue( value
);
10164 // Only allow setting values that are actually present in the dropdown
10165 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
10166 this.radioSelectWidget
.findFirstSelectableItem();
10167 this.radioSelectWidget
.selectItem( selected
);
10168 value
= selected
? selected
.getData() : '';
10169 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10176 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10177 this.radioSelectWidget
.setDisabled( state
);
10178 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10183 * Set the options available for this input.
10185 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10187 * @return {OO.ui.Widget} The widget, for chaining
10189 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10190 var value
= this.getValue();
10192 this.setOptionsData( options
);
10194 // Re-set the value to update the visible interface (RadioSelectWidget).
10195 // In case the previous value is no longer an available option, select the first valid one.
10196 this.setValue( value
);
10202 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10204 * This method may be called before the parent constructor, so various properties may not be
10207 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10210 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10213 this.radioSelectWidget
10215 .addItems( options
.map( function ( opt
) {
10216 var optValue
= widget
.cleanUpValue( opt
.data
);
10217 return new OO
.ui
.RadioOptionWidget( {
10219 label
: opt
.label
!== undefined ? opt
.label
: optValue
10227 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10228 this.radioSelectWidget
.focus();
10235 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10236 this.radioSelectWidget
.blur();
10241 * CheckboxMultiselectInputWidget is a
10242 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10243 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10244 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10245 * more information about input widgets.
10248 * // A CheckboxMultiselectInputWidget with three options.
10249 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10251 * { data: 'a', label: 'First' },
10252 * { data: 'b', label: 'Second' },
10253 * { data: 'c', label: 'Third' }
10256 * $( document.body ).append( multiselectInput.$element );
10258 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10261 * @extends OO.ui.InputWidget
10264 * @param {Object} [config] Configuration options
10265 * @cfg {Object[]} [options=[]] Array of menu options in the format
10266 * `{ data: …, label: …, disabled: … }`
10268 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10269 // Configuration initialization
10270 config
= config
|| {};
10272 // Properties (must be done before parent constructor which calls #setDisabled)
10273 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10274 // Must be set before the #setOptionsData call below
10275 this.inputName
= config
.name
;
10276 // Set up the options before parent constructor, which uses them to validate config.value.
10277 // Use this instead of setOptions() because this.$input is not set up yet
10278 this.setOptionsData( config
.options
|| [] );
10280 // Parent constructor
10281 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10284 this.checkboxMultiselectWidget
.connect( this, {
10285 select
: 'onCheckboxesSelect'
10290 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10291 .append( this.checkboxMultiselectWidget
.$element
);
10292 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10293 this.$input
.detach();
10298 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10300 /* Static Methods */
10305 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10306 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState(
10309 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10310 .toArray().map( function ( el
) { return el
.value
; } );
10317 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10318 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10319 // Cannot reuse the `<input type=checkbox>` set
10320 delete config
.$input
;
10330 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10332 return $( '<unused>' );
10336 * Handles CheckboxMultiselectWidget select events.
10340 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10341 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10347 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10348 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10349 .toArray().map( function ( el
) { return el
.value
; } );
10350 if ( this.value
!== value
) {
10351 this.setValue( value
);
10359 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10360 value
= this.cleanUpValue( value
);
10361 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10362 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10363 if ( this.optionsDirty
) {
10364 // We reached this from the constructor or from #setOptions.
10365 // We have to update the <select> element.
10366 this.updateOptionsInterface();
10372 * Clean up incoming value.
10374 * @param {string[]} value Original value
10375 * @return {string[]} Cleaned up value
10377 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10378 var i
, singleValue
,
10380 if ( !Array
.isArray( value
) ) {
10383 for ( i
= 0; i
< value
.length
; i
++ ) {
10384 singleValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10385 .call( this, value
[ i
] );
10386 // Remove options that we don't have here
10387 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10390 cleanValue
.push( singleValue
);
10398 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10399 this.checkboxMultiselectWidget
.setDisabled( state
);
10400 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10405 * Set the options available for this input.
10407 * @param {Object[]} options Array of menu options in the format
10408 * `{ data: …, label: …, disabled: … }`
10410 * @return {OO.ui.Widget} The widget, for chaining
10412 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10413 var value
= this.getValue();
10415 this.setOptionsData( options
);
10417 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10418 // This will also get rid of any stale options that we just removed.
10419 this.setValue( value
);
10425 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10427 * This method may be called before the parent constructor, so various properties may not be
10430 * @param {Object[]} options Array of menu options in the format
10431 * `{ data: …, label: … }`
10434 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10437 this.optionsDirty
= true;
10439 this.checkboxMultiselectWidget
10441 .addItems( options
.map( function ( opt
) {
10442 var optValue
, item
, optDisabled
;
10443 optValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10444 .call( widget
, opt
.data
);
10445 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10446 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10448 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10449 disabled
: optDisabled
10451 // Set the 'name' and 'value' for form submission
10452 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10453 item
.checkbox
.setValue( optValue
);
10459 * Update the user-visible interface to match the internal list of options and value.
10461 * This method must only be called after the parent constructor.
10465 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10466 var defaultValue
= this.defaultValue
;
10468 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10469 // Remember original selection state. This property can be later used to check whether
10470 // the selection state of the input has been changed since it was created.
10471 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10472 item
.checkbox
.defaultSelected
= isDefault
;
10473 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10476 this.optionsDirty
= false;
10482 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10483 this.checkboxMultiselectWidget
.focus();
10488 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10489 * size of the field as well as its presentation. In addition, these widgets can be configured
10490 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an
10491 * optional validation-pattern (used to determine if an input value is valid or not) and an input
10492 * filter, which modifies incoming values rather than validating them.
10493 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10495 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10498 * // A TextInputWidget.
10499 * var textInput = new OO.ui.TextInputWidget( {
10500 * value: 'Text input'
10502 * $( document.body ).append( textInput.$element );
10504 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10507 * @extends OO.ui.InputWidget
10508 * @mixins OO.ui.mixin.IconElement
10509 * @mixins OO.ui.mixin.IndicatorElement
10510 * @mixins OO.ui.mixin.PendingElement
10511 * @mixins OO.ui.mixin.LabelElement
10512 * @mixins OO.ui.mixin.FlaggedElement
10515 * @param {Object} [config] Configuration options
10516 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10517 * 'email', 'url' or 'number'.
10518 * @cfg {string} [placeholder] Placeholder text
10519 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10520 * instruct the browser to focus this widget.
10521 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10522 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10524 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10525 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10526 * many emojis) count as 2 characters each.
10527 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10528 * the value or placeholder text: `'before'` or `'after'`
10529 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator:
10530 * 'required'`. Note that `false` & setting `indicator: 'required' will result in no indicator
10532 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10533 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined`
10534 * means leaving it up to the browser).
10535 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10536 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10537 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10538 * value for it to be considered valid; when Function, a function receiving the value as parameter
10539 * that must return true, or promise resolving to true, for it to be considered valid.
10541 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10542 // Configuration initialization
10543 config
= $.extend( {
10545 labelPosition
: 'after'
10548 // Parent constructor
10549 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10551 // Mixin constructors
10552 OO
.ui
.mixin
.IconElement
.call( this, config
);
10553 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10554 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( { $pending
: this.$input
}, config
) );
10555 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10556 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
10559 this.type
= this.getSaneType( config
);
10560 this.readOnly
= false;
10561 this.required
= false;
10562 this.validate
= null;
10563 this.scrollWidth
= null;
10565 this.setValidation( config
.validate
);
10566 this.setLabelPosition( config
.labelPosition
);
10570 keypress
: this.onKeyPress
.bind( this ),
10571 blur
: this.onBlur
.bind( this ),
10572 focus
: this.onFocus
.bind( this )
10574 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10575 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10576 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10577 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10581 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10582 .append( this.$icon
, this.$indicator
);
10583 this.setReadOnly( !!config
.readOnly
);
10584 this.setRequired( !!config
.required
);
10585 if ( config
.placeholder
!== undefined ) {
10586 this.$input
.attr( 'placeholder', config
.placeholder
);
10588 if ( config
.maxLength
!== undefined ) {
10589 this.$input
.attr( 'maxlength', config
.maxLength
);
10591 if ( config
.autofocus
) {
10592 this.$input
.attr( 'autofocus', 'autofocus' );
10594 if ( config
.autocomplete
=== false ) {
10595 this.$input
.attr( 'autocomplete', 'off' );
10596 // Turning off autocompletion also disables "form caching" when the user navigates to a
10597 // different page and then clicks "Back". Re-enable it when leaving.
10598 // Borrowed from jQuery UI.
10600 beforeunload: function () {
10601 this.$input
.removeAttr( 'autocomplete' );
10603 pageshow: function () {
10604 // Browsers don't seem to actually fire this event on "Back", they instead just
10605 // reload the whole page... it shouldn't hurt, though.
10606 this.$input
.attr( 'autocomplete', 'off' );
10610 if ( config
.spellcheck
!== undefined ) {
10611 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10613 if ( this.label
) {
10614 this.isWaitingToBeAttached
= true;
10615 this.installParentChangeDetector();
10621 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10622 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10623 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10624 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10625 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10626 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
10628 /* Static Properties */
10630 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10638 * An `enter` event is emitted when the user presses Enter key inside the text box.
10646 * Handle icon mouse down events.
10649 * @param {jQuery.Event} e Mouse down event
10650 * @return {undefined/boolean} False to prevent default if event is handled
10652 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10653 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10660 * Handle indicator mouse down events.
10663 * @param {jQuery.Event} e Mouse down event
10664 * @return {undefined/boolean} False to prevent default if event is handled
10666 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10667 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10674 * Handle key press events.
10677 * @param {jQuery.Event} e Key press event
10678 * @fires enter If Enter key is pressed
10680 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10681 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10682 this.emit( 'enter', e
);
10687 * Handle blur events.
10690 * @param {jQuery.Event} e Blur event
10692 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10693 this.setValidityFlag();
10697 * Handle focus events.
10700 * @param {jQuery.Event} e Focus event
10702 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10703 if ( this.isWaitingToBeAttached
) {
10704 // If we've received focus, then we must be attached to the document, and if
10705 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10706 this.onElementAttach();
10708 this.setValidityFlag( true );
10712 * Handle element attach events.
10715 * @param {jQuery.Event} e Element attach event
10717 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10718 this.isWaitingToBeAttached
= false;
10719 // Any previously calculated size is now probably invalid if we reattached elsewhere
10720 this.valCache
= null;
10721 this.positionLabel();
10725 * Handle debounced change events.
10727 * @param {string} value
10730 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10731 this.setValidityFlag();
10735 * Check if the input is {@link #readOnly read-only}.
10737 * @return {boolean}
10739 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10740 return this.readOnly
;
10744 * Set the {@link #readOnly read-only} state of the input.
10746 * @param {boolean} state Make input read-only
10748 * @return {OO.ui.Widget} The widget, for chaining
10750 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10751 this.readOnly
= !!state
;
10752 this.$input
.prop( 'readOnly', this.readOnly
);
10757 * Check if the input is {@link #required required}.
10759 * @return {boolean}
10761 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10762 return this.required
;
10766 * Set the {@link #required required} state of the input.
10768 * @param {boolean} state Make input required
10770 * @return {OO.ui.Widget} The widget, for chaining
10772 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10773 this.required
= !!state
;
10774 if ( this.required
) {
10776 .prop( 'required', true )
10777 .attr( 'aria-required', 'true' );
10778 if ( this.getIndicator() === null ) {
10779 this.setIndicator( 'required' );
10783 .prop( 'required', false )
10784 .removeAttr( 'aria-required' );
10785 if ( this.getIndicator() === 'required' ) {
10786 this.setIndicator( null );
10793 * Support function for making #onElementAttach work across browsers.
10795 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10796 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10798 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10799 * first time that the element gets attached to the documented.
10801 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10802 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10803 MutationObserver
= window
.MutationObserver
||
10804 window
.WebKitMutationObserver
||
10805 window
.MozMutationObserver
,
10808 if ( MutationObserver
) {
10809 // The new way. If only it wasn't so ugly.
10811 if ( this.isElementAttached() ) {
10812 // Widget is attached already, do nothing. This breaks the functionality of this
10813 // function when the widget is detached and reattached. Alas, doing this correctly with
10814 // MutationObserver would require observation of the whole document, which would hurt
10815 // performance of other, more important code.
10819 // Find topmost node in the tree
10820 topmostNode
= this.$element
[ 0 ];
10821 while ( topmostNode
.parentNode
) {
10822 topmostNode
= topmostNode
.parentNode
;
10825 // We have no way to detect the $element being attached somewhere without observing the
10826 // entire DOM with subtree modifications, which would hurt performance. So we cheat: we hook
10827 // to the parent node of $element, and instead detect when $element is removed from it (and
10828 // thus probably attached somewhere else). If there is no parent, we create a "fake" one. If
10829 // it doesn't get attached, we end up back here and create the parent.
10830 mutationObserver
= new MutationObserver( function ( mutations
) {
10831 var i
, j
, removedNodes
;
10832 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10833 removedNodes
= mutations
[ i
].removedNodes
;
10834 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10835 if ( removedNodes
[ j
] === topmostNode
) {
10836 setTimeout( onRemove
, 0 );
10843 onRemove = function () {
10844 // If the node was attached somewhere else, report it
10845 if ( widget
.isElementAttached() ) {
10846 widget
.onElementAttach();
10848 mutationObserver
.disconnect();
10849 widget
.installParentChangeDetector();
10852 // Create a fake parent and observe it
10853 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10854 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10856 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10857 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10858 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10866 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10867 if ( this.getSaneType( config
) === 'number' ) {
10868 return $( '<input>' )
10869 .attr( 'step', 'any' )
10870 .attr( 'type', 'number' );
10872 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10877 * Get sanitized value for 'type' for given config.
10879 * @param {Object} config Configuration options
10880 * @return {string|null}
10883 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10884 var allowedTypes
= [
10891 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10895 * Focus the input and select a specified range within the text.
10897 * @param {number} from Select from offset
10898 * @param {number} [to] Select to offset, defaults to from
10900 * @return {OO.ui.Widget} The widget, for chaining
10902 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10903 var isBackwards
, start
, end
,
10904 input
= this.$input
[ 0 ];
10908 isBackwards
= to
< from;
10909 start
= isBackwards
? to
: from;
10910 end
= isBackwards
? from : to
;
10915 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10917 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10918 // Rather than expensively check if the input is attached every time, just check
10919 // if it was the cause of an error being thrown. If not, rethrow the error.
10920 if ( this.getElementDocument().body
.contains( input
) ) {
10928 * Get an object describing the current selection range in a directional manner
10930 * @return {Object} Object containing 'from' and 'to' offsets
10932 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10933 var input
= this.$input
[ 0 ],
10934 start
= input
.selectionStart
,
10935 end
= input
.selectionEnd
,
10936 isBackwards
= input
.selectionDirection
=== 'backward';
10939 from: isBackwards
? end
: start
,
10940 to
: isBackwards
? start
: end
10945 * Get the length of the text input value.
10947 * This could differ from the length of #getValue if the
10948 * value gets filtered
10950 * @return {number} Input length
10952 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10953 return this.$input
[ 0 ].value
.length
;
10957 * Focus the input and select the entire text.
10960 * @return {OO.ui.Widget} The widget, for chaining
10962 OO
.ui
.TextInputWidget
.prototype.select = function () {
10963 return this.selectRange( 0, this.getInputLength() );
10967 * Focus the input and move the cursor to the start.
10970 * @return {OO.ui.Widget} The widget, for chaining
10972 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10973 return this.selectRange( 0 );
10977 * Focus the input and move the cursor to the end.
10980 * @return {OO.ui.Widget} The widget, for chaining
10982 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10983 return this.selectRange( this.getInputLength() );
10987 * Insert new content into the input.
10989 * @param {string} content Content to be inserted
10991 * @return {OO.ui.Widget} The widget, for chaining
10993 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10995 range
= this.getRange(),
10996 value
= this.getValue();
10998 start
= Math
.min( range
.from, range
.to
);
10999 end
= Math
.max( range
.from, range
.to
);
11001 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
11002 this.selectRange( start
+ content
.length
);
11007 * Insert new content either side of a selection.
11009 * @param {string} pre Content to be inserted before the selection
11010 * @param {string} post Content to be inserted after the selection
11012 * @return {OO.ui.Widget} The widget, for chaining
11014 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
11016 range
= this.getRange(),
11017 offset
= pre
.length
;
11019 start
= Math
.min( range
.from, range
.to
);
11020 end
= Math
.max( range
.from, range
.to
);
11022 this.selectRange( start
).insertContent( pre
);
11023 this.selectRange( offset
+ end
).insertContent( post
);
11025 this.selectRange( offset
+ start
, offset
+ end
);
11030 * Set the validation pattern.
11032 * The validation pattern is either a regular expression, a function, or the symbolic name of a
11033 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11034 * value must contain only numbers).
11036 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
11037 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11039 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
11040 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
11041 this.validate
= validate
;
11043 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
11048 * Sets the 'invalid' flag appropriately.
11050 * @param {boolean} [isValid] Optionally override validation result
11052 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
11054 setFlag = function ( valid
) {
11056 widget
.$input
.attr( 'aria-invalid', 'true' );
11058 widget
.$input
.removeAttr( 'aria-invalid' );
11060 widget
.setFlags( { invalid
: !valid
} );
11063 if ( isValid
!== undefined ) {
11064 setFlag( isValid
);
11066 this.getValidity().then( function () {
11075 * Get the validity of current value.
11077 * This method returns a promise that resolves if the value is valid and rejects if
11078 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
11080 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
11082 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
11085 function rejectOrResolve( valid
) {
11087 return $.Deferred().resolve().promise();
11089 return $.Deferred().reject().promise();
11093 // Check browser validity and reject if it is invalid
11095 this.$input
[ 0 ].checkValidity
!== undefined &&
11096 this.$input
[ 0 ].checkValidity() === false
11098 return rejectOrResolve( false );
11101 // Run our checks if the browser thinks the field is valid
11102 if ( this.validate
instanceof Function
) {
11103 result
= this.validate( this.getValue() );
11104 if ( result
&& typeof result
.promise
=== 'function' ) {
11105 return result
.promise().then( function ( valid
) {
11106 return rejectOrResolve( valid
);
11109 return rejectOrResolve( result
);
11112 return rejectOrResolve( this.getValue().match( this.validate
) );
11117 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11119 * @param {string} labelPosition Label position, 'before' or 'after'
11121 * @return {OO.ui.Widget} The widget, for chaining
11123 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
11124 this.labelPosition
= labelPosition
;
11125 if ( this.label
) {
11126 // If there is no label and we only change the position, #updatePosition is a no-op,
11127 // but it takes really a lot of work to do nothing.
11128 this.updatePosition();
11134 * Update the position of the inline label.
11136 * This method is called by #setLabelPosition, and can also be called on its own if
11137 * something causes the label to be mispositioned.
11140 * @return {OO.ui.Widget} The widget, for chaining
11142 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
11143 var after
= this.labelPosition
=== 'after';
11146 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
11147 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
11149 this.valCache
= null;
11150 this.scrollWidth
= null;
11151 this.positionLabel();
11157 * Position the label by setting the correct padding on the input.
11161 * @return {OO.ui.Widget} The widget, for chaining
11163 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11164 var after
, rtl
, property
, newCss
;
11166 if ( this.isWaitingToBeAttached
) {
11167 // #onElementAttach will be called soon, which calls this method
11172 'padding-right': '',
11176 if ( this.label
) {
11177 this.$element
.append( this.$label
);
11179 this.$label
.detach();
11180 // Clear old values if present
11181 this.$input
.css( newCss
);
11185 after
= this.labelPosition
=== 'after';
11186 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11187 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11189 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11190 // We have to clear the padding on the other side, in case the element direction changed
11191 this.$input
.css( newCss
);
11197 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11198 * {@link OO.ui.mixin.IconElement search icon} by default.
11199 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11201 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11204 * @extends OO.ui.TextInputWidget
11207 * @param {Object} [config] Configuration options
11209 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11210 config
= $.extend( {
11214 // Parent constructor
11215 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11218 this.connect( this, {
11221 this.$indicator
.on( 'click', this.onIndicatorClick
.bind( this ) );
11224 this.updateSearchIndicator();
11225 this.connect( this, {
11226 disable
: 'onDisable'
11232 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11240 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11245 * Handle click events on the indicator
11247 * @param {jQuery.Event} e Click event
11248 * @return {boolean}
11250 OO
.ui
.SearchInputWidget
.prototype.onIndicatorClick = function ( e
) {
11251 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11252 // Clear the text field
11253 this.setValue( '' );
11260 * Update the 'clear' indicator displayed on type: 'search' text
11261 * fields, hiding it when the field is already empty or when it's not
11264 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11265 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11266 this.setIndicator( null );
11268 this.setIndicator( 'clear' );
11273 * Handle change events.
11277 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11278 this.updateSearchIndicator();
11282 * Handle disable events.
11284 * @param {boolean} disabled Element is disabled
11287 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11288 this.updateSearchIndicator();
11294 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11295 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11296 this.updateSearchIndicator();
11301 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11302 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11303 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11304 * {@link OO.ui.mixin.IndicatorElement indicators}.
11305 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11307 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11310 * // A MultilineTextInputWidget.
11311 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11312 * value: 'Text input on multiple lines'
11314 * $( document.body ).append( multilineTextInput.$element );
11316 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11319 * @extends OO.ui.TextInputWidget
11322 * @param {Object} [config] Configuration options
11323 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11324 * specifies minimum number of rows to display.
11325 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11326 * Use the #maxRows config to specify a maximum number of displayed rows.
11327 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11328 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11330 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11331 config
= $.extend( {
11334 // Parent constructor
11335 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11338 this.autosize
= !!config
.autosize
;
11339 this.styleHeight
= null;
11340 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11341 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11343 // Clone for resizing
11344 if ( this.autosize
) {
11345 this.$clone
= this.$input
11347 .removeAttr( 'id' )
11348 .removeAttr( 'name' )
11349 .insertAfter( this.$input
)
11350 .attr( 'aria-hidden', 'true' )
11351 .addClass( 'oo-ui-element-hidden' );
11355 this.connect( this, {
11360 if ( config
.rows
) {
11361 this.$input
.attr( 'rows', config
.rows
);
11363 if ( this.autosize
) {
11364 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11365 this.isWaitingToBeAttached
= true;
11366 this.installParentChangeDetector();
11372 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11374 /* Static Methods */
11379 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11380 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11381 state
.scrollTop
= config
.$input
.scrollTop();
11390 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11391 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11396 * Handle change events.
11400 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11407 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11408 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11415 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11417 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11419 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11420 // Some platforms emit keycode 10 for Control+Enter keypress in a textarea
11423 this.emit( 'enter', e
);
11428 * Automatically adjust the size of the text input.
11430 * This only affects multiline inputs that are {@link #autosize autosized}.
11433 * @return {OO.ui.Widget} The widget, for chaining
11436 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11437 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11438 idealHeight
, newHeight
, scrollWidth
, property
;
11440 if ( this.$input
.val() !== this.valCache
) {
11441 if ( this.autosize
) {
11443 .val( this.$input
.val() )
11444 .attr( 'rows', this.minRows
)
11445 // Set inline height property to 0 to measure scroll height
11446 .css( 'height', 0 );
11448 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11450 this.valCache
= this.$input
.val();
11452 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11454 // Remove inline height property to measure natural heights
11455 this.$clone
.css( 'height', '' );
11456 innerHeight
= this.$clone
.innerHeight();
11457 outerHeight
= this.$clone
.outerHeight();
11459 // Measure max rows height
11461 .attr( 'rows', this.maxRows
)
11462 .css( 'height', 'auto' )
11464 maxInnerHeight
= this.$clone
.innerHeight();
11466 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11467 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11468 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11469 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11471 this.$clone
.addClass( 'oo-ui-element-hidden' );
11473 // Only apply inline height when expansion beyond natural height is needed
11474 // Use the difference between the inner and outer height as a buffer
11475 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11476 if ( newHeight
!== this.styleHeight
) {
11477 this.$input
.css( 'height', newHeight
);
11478 this.styleHeight
= newHeight
;
11479 this.emit( 'resize' );
11482 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11483 if ( scrollWidth
!== this.scrollWidth
) {
11484 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11486 this.$label
.css( { right
: '', left
: '' } );
11487 this.$indicator
.css( { right
: '', left
: '' } );
11489 if ( scrollWidth
) {
11490 this.$indicator
.css( property
, scrollWidth
);
11491 if ( this.labelPosition
=== 'after' ) {
11492 this.$label
.css( property
, scrollWidth
);
11496 this.scrollWidth
= scrollWidth
;
11497 this.positionLabel();
11507 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11508 return $( '<textarea>' );
11512 * Check if the input automatically adjusts its size.
11514 * @return {boolean}
11516 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11517 return !!this.autosize
;
11523 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11524 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11525 if ( state
.scrollTop
!== undefined ) {
11526 this.$input
.scrollTop( state
.scrollTop
);
11531 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11532 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11533 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11535 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11536 * option, that option will appear to be selected.
11537 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11540 * After the user chooses an option, its `data` will be used as a new value for the widget.
11541 * A `label` also can be specified for each option: if given, it will be shown instead of the
11542 * `data` in the dropdown menu.
11544 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11546 * For more information about menus and options, please see the
11547 * [OOUI documentation on MediaWiki][1].
11550 * // A ComboBoxInputWidget.
11551 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11552 * value: 'Option 1',
11554 * { data: 'Option 1' },
11555 * { data: 'Option 2' },
11556 * { data: 'Option 3' }
11559 * $( document.body ).append( comboBox.$element );
11562 * // Example: A ComboBoxInputWidget with additional option labels.
11563 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11564 * value: 'Option 1',
11567 * data: 'Option 1',
11568 * label: 'Option One'
11571 * data: 'Option 2',
11572 * label: 'Option Two'
11575 * data: 'Option 3',
11576 * label: 'Option Three'
11580 * $( document.body ).append( comboBox.$element );
11582 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11585 * @extends OO.ui.TextInputWidget
11588 * @param {Object} [config] Configuration options
11589 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11590 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu
11592 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
11593 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
11594 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
11595 * uses relative positioning.
11596 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11598 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11599 // Configuration initialization
11600 config
= $.extend( {
11601 autocomplete
: false
11604 // ComboBoxInputWidget shouldn't support `multiline`
11605 config
.multiline
= false;
11607 // See InputWidget#reusePreInfuseDOM about `config.$input`
11608 if ( config
.$input
) {
11609 config
.$input
.removeAttr( 'list' );
11612 // Parent constructor
11613 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11616 this.$overlay
= ( config
.$overlay
=== true ?
11617 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11618 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11619 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11620 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11622 invisibleLabel
: true,
11623 disabled
: this.disabled
11625 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11629 $floatableContainer
: this.$element
,
11630 disabled
: this.isDisabled()
11636 this.connect( this, {
11637 change
: 'onInputChange',
11638 enter
: 'onInputEnter'
11640 this.dropdownButton
.connect( this, {
11641 click
: 'onDropdownButtonClick'
11643 this.menu
.connect( this, {
11644 choose
: 'onMenuChoose',
11645 add
: 'onMenuItemsChange',
11646 remove
: 'onMenuItemsChange',
11647 toggle
: 'onMenuToggle'
11651 this.$input
.attr( {
11653 'aria-owns': this.menu
.getElementId(),
11654 'aria-autocomplete': 'list'
11656 this.dropdownButton
.$button
.attr( {
11657 'aria-controls': this.menu
.getElementId()
11659 // Do not override options set via config.menu.items
11660 if ( config
.options
!== undefined ) {
11661 this.setOptions( config
.options
);
11663 this.$field
= $( '<div>' )
11664 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11665 .append( this.$input
, this.dropdownButton
.$element
);
11667 .addClass( 'oo-ui-comboBoxInputWidget' )
11668 .append( this.$field
);
11669 this.$overlay
.append( this.menu
.$element
);
11670 this.onMenuItemsChange();
11675 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11680 * Get the combobox's menu.
11682 * @return {OO.ui.MenuSelectWidget} Menu widget
11684 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11689 * Get the combobox's text input widget.
11691 * @return {OO.ui.TextInputWidget} Text input widget
11693 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11698 * Handle input change events.
11701 * @param {string} value New value
11703 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11704 var match
= this.menu
.findItemFromData( value
);
11706 this.menu
.selectItem( match
);
11707 if ( this.menu
.findHighlightedItem() ) {
11708 this.menu
.highlightItem( match
);
11711 if ( !this.isDisabled() ) {
11712 this.menu
.toggle( true );
11717 * Handle input enter events.
11721 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11722 if ( !this.isDisabled() ) {
11723 this.menu
.toggle( false );
11728 * Handle button click events.
11732 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11733 this.menu
.toggle();
11738 * Handle menu choose events.
11741 * @param {OO.ui.OptionWidget} item Chosen item
11743 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11744 this.setValue( item
.getData() );
11748 * Handle menu item change events.
11752 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11753 var match
= this.menu
.findItemFromData( this.getValue() );
11754 this.menu
.selectItem( match
);
11755 if ( this.menu
.findHighlightedItem() ) {
11756 this.menu
.highlightItem( match
);
11758 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11762 * Handle menu toggle events.
11765 * @param {boolean} isVisible Open state of the menu
11767 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11768 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11772 * Update the disabled state of the controls
11776 * @return {OO.ui.ComboBoxInputWidget} The widget, for chaining
11778 OO
.ui
.ComboBoxInputWidget
.prototype.updateControlsDisabled = function () {
11779 var disabled
= this.isDisabled() || this.isReadOnly();
11780 if ( this.dropdownButton
) {
11781 this.dropdownButton
.setDisabled( disabled
);
11784 this.menu
.setDisabled( disabled
);
11792 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function () {
11794 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.apply( this, arguments
);
11795 this.updateControlsDisabled();
11802 OO
.ui
.ComboBoxInputWidget
.prototype.setReadOnly = function () {
11804 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
11805 this.updateControlsDisabled();
11810 * Set the options available for this input.
11812 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11814 * @return {OO.ui.Widget} The widget, for chaining
11816 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11819 .addItems( options
.map( function ( opt
) {
11820 return new OO
.ui
.MenuOptionWidget( {
11822 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11830 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11831 * which is a widget that is specified by reference before any optional configuration settings.
11833 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of
11836 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11837 * A left-alignment is used for forms with many fields.
11838 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11839 * A right-alignment is used for long but familiar forms which users tab through,
11840 * verifying the current field with a quick glance at the label.
11841 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11842 * that users fill out from top to bottom.
11843 * - **inline**: The label is placed after the field-widget and aligned to the left.
11844 * An inline-alignment is best used with checkboxes or radio buttons.
11846 * Help text can either be:
11848 * - accessed via a help icon that appears in the upper right corner of the rendered field layout,
11850 * - shown as a subtle explanation below the label.
11852 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`.
11853 * If it is long or not essential, leave `helpInline` to its default, `false`.
11855 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11857 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11860 * @extends OO.ui.Layout
11861 * @mixins OO.ui.mixin.LabelElement
11862 * @mixins OO.ui.mixin.TitledElement
11865 * @param {OO.ui.Widget} fieldWidget Field widget
11866 * @param {Object} [config] Configuration options
11867 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11869 * @cfg {Array} [errors] Error messages about the widget, which will be
11870 * displayed below the widget.
11871 * @cfg {Array} [warnings] Warning messages about the widget, which will be
11872 * displayed below the widget.
11873 * @cfg {Array} [successMessages] Success messages on user interactions with the widget,
11874 * which will be displayed below the widget.
11875 * The array may contain strings or OO.ui.HtmlSnippet instances.
11876 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11877 * below the widget.
11878 * The array may contain strings or OO.ui.HtmlSnippet instances.
11879 * These are more visible than `help` messages when `helpInline` is set, and so
11880 * might be good for transient messages.
11881 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11882 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11883 * corner of the rendered field; clicking it will display the text in a popup.
11884 * If `helpInline` is `true`, then a subtle description will be shown after the
11886 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11887 * or shown when the "help" icon is clicked.
11888 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11890 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11892 * @throws {Error} An error is thrown if no widget is specified
11894 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11895 // Allow passing positional parameters inside the config object
11896 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11897 config
= fieldWidget
;
11898 fieldWidget
= config
.fieldWidget
;
11901 // Make sure we have required constructor arguments
11902 if ( fieldWidget
=== undefined ) {
11903 throw new Error( 'Widget not found' );
11906 // Configuration initialization
11907 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11909 // Parent constructor
11910 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11912 // Mixin constructors
11913 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
11914 $label
: $( '<label>' )
11916 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( { $titled
: this.$label
}, config
) );
11919 this.fieldWidget
= fieldWidget
;
11921 this.warnings
= [];
11922 this.successMessages
= [];
11924 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11925 this.$messages
= $( '<ul>' );
11926 this.$header
= $( '<span>' );
11927 this.$body
= $( '<div>' );
11929 this.helpInline
= config
.helpInline
;
11932 this.fieldWidget
.connect( this, {
11933 disable
: 'onFieldDisable'
11937 this.$help
= config
.help
?
11938 this.createHelpElement( config
.help
, config
.$overlay
) :
11940 if ( this.fieldWidget
.getInputId() ) {
11941 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11942 if ( this.helpInline
) {
11943 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
11946 this.$label
.on( 'click', function () {
11947 this.fieldWidget
.simulateLabelClick();
11949 if ( this.helpInline
) {
11950 this.$help
.on( 'click', function () {
11951 this.fieldWidget
.simulateLabelClick();
11956 .addClass( 'oo-ui-fieldLayout' )
11957 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11958 .append( this.$body
);
11959 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11960 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11961 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11963 .addClass( 'oo-ui-fieldLayout-field' )
11964 .append( this.fieldWidget
.$element
);
11966 this.setErrors( config
.errors
|| [] );
11967 this.setWarnings( config
.warnings
|| [] );
11968 this.setSuccess( config
.successMessages
|| [] );
11969 this.setNotices( config
.notices
|| [] );
11970 this.setAlignment( config
.align
);
11971 // Call this again to take into account the widget's accessKey
11972 this.updateTitle();
11977 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11978 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11979 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11984 * Handle field disable events.
11987 * @param {boolean} value Field is disabled
11989 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11990 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11994 * Get the widget contained by the field.
11996 * @return {OO.ui.Widget} Field widget
11998 OO
.ui
.FieldLayout
.prototype.getField = function () {
11999 return this.fieldWidget
;
12003 * Return `true` if the given field widget can be used with `'inline'` alignment (see
12004 * #setAlignment). Return `false` if it can't or if this can't be determined.
12006 * @return {boolean}
12008 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
12009 // This is very simplistic, but should be good enough.
12010 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
12015 * @param {string} kind 'error' or 'notice'
12016 * @param {string|OO.ui.HtmlSnippet} text
12019 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
12020 var $listItem
, $icon
, message
;
12021 $listItem
= $( '<li>' );
12022 if ( kind
=== 'error' ) {
12023 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'error' ] } ).$element
;
12024 $listItem
.attr( 'role', 'alert' );
12025 } else if ( kind
=== 'warning' ) {
12026 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
12027 $listItem
.attr( 'role', 'alert' );
12028 } else if ( kind
=== 'success' ) {
12029 $icon
= new OO
.ui
.IconWidget( { icon
: 'check', flags
: [ 'success' ] } ).$element
;
12030 } else if ( kind
=== 'notice' ) {
12031 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
12035 message
= new OO
.ui
.LabelWidget( { label
: text
} );
12037 .append( $icon
, message
.$element
)
12038 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
12043 * Set the field alignment mode.
12046 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
12048 * @return {OO.ui.BookletLayout} The layout, for chaining
12050 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
12051 if ( value
!== this.align
) {
12052 // Default to 'left'
12053 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
12057 if ( value
=== 'inline' && !this.isFieldInline() ) {
12060 // Reorder elements
12062 if ( this.helpInline
) {
12063 if ( value
=== 'top' ) {
12064 this.$header
.append( this.$label
);
12065 this.$body
.append( this.$header
, this.$field
, this.$help
);
12066 } else if ( value
=== 'inline' ) {
12067 this.$header
.append( this.$label
, this.$help
);
12068 this.$body
.append( this.$field
, this.$header
);
12070 this.$header
.append( this.$label
, this.$help
);
12071 this.$body
.append( this.$header
, this.$field
);
12074 if ( value
=== 'top' ) {
12075 this.$header
.append( this.$help
, this.$label
);
12076 this.$body
.append( this.$header
, this.$field
);
12077 } else if ( value
=== 'inline' ) {
12078 this.$header
.append( this.$help
, this.$label
);
12079 this.$body
.append( this.$field
, this.$header
);
12081 this.$header
.append( this.$label
);
12082 this.$body
.append( this.$header
, this.$help
, this.$field
);
12085 // Set classes. The following classes can be used here:
12086 // * oo-ui-fieldLayout-align-left
12087 // * oo-ui-fieldLayout-align-right
12088 // * oo-ui-fieldLayout-align-top
12089 // * oo-ui-fieldLayout-align-inline
12090 if ( this.align
) {
12091 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
12093 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
12094 this.align
= value
;
12101 * Set the list of error messages.
12103 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
12104 * The array may contain strings or OO.ui.HtmlSnippet instances.
12106 * @return {OO.ui.BookletLayout} The layout, for chaining
12108 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
12109 this.errors
= errors
.slice();
12110 this.updateMessages();
12115 * Set the list of warning messages.
12117 * @param {Array} warnings Warning messages about the widget, which will be displayed below
12119 * The array may contain strings or OO.ui.HtmlSnippet instances.
12121 * @return {OO.ui.BookletLayout} The layout, for chaining
12123 OO
.ui
.FieldLayout
.prototype.setWarnings = function ( warnings
) {
12124 this.warnings
= warnings
.slice();
12125 this.updateMessages();
12130 * Set the list of success messages.
12132 * @param {Array} successMessages Success messages about the widget, which will be displayed below
12134 * The array may contain strings or OO.ui.HtmlSnippet instances.
12136 * @return {OO.ui.BookletLayout} The layout, for chaining
12138 OO
.ui
.FieldLayout
.prototype.setSuccess = function ( successMessages
) {
12139 this.successMessages
= successMessages
.slice();
12140 this.updateMessages();
12145 * Set the list of notice messages.
12147 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
12148 * The array may contain strings or OO.ui.HtmlSnippet instances.
12150 * @return {OO.ui.BookletLayout} The layout, for chaining
12152 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
12153 this.notices
= notices
.slice();
12154 this.updateMessages();
12159 * Update the rendering of error, warning, success and notice messages.
12163 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
12165 this.$messages
.empty();
12168 this.errors
.length
||
12169 this.warnings
.length
||
12170 this.successMessages
.length
||
12171 this.notices
.length
12173 this.$body
.after( this.$messages
);
12175 this.$messages
.remove();
12179 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
12180 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
12182 for ( i
= 0; i
< this.warnings
.length
; i
++ ) {
12183 this.$messages
.append( this.makeMessage( 'warning', this.warnings
[ i
] ) );
12185 for ( i
= 0; i
< this.successMessages
.length
; i
++ ) {
12186 this.$messages
.append( this.makeMessage( 'success', this.successMessages
[ i
] ) );
12188 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
12189 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
12194 * Include information about the widget's accessKey in our title. TitledElement calls this method.
12195 * (This is a bit of a hack.)
12198 * @param {string} title Tooltip label for 'title' attribute
12201 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
12202 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
12203 return this.fieldWidget
.formatTitleWithAccessKey( title
);
12209 * Creates and returns the help element. Also sets the `aria-describedby`
12210 * attribute on the main element of the `fieldWidget`.
12213 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
12214 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
12215 * @return {jQuery} The element that should become `this.$help`.
12217 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
12218 var helpId
, helpWidget
;
12220 if ( this.helpInline
) {
12221 helpWidget
= new OO
.ui
.LabelWidget( {
12223 classes
: [ 'oo-ui-inline-help' ]
12226 helpId
= helpWidget
.getElementId();
12228 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12229 $overlay
: $overlay
,
12233 classes
: [ 'oo-ui-fieldLayout-help' ],
12236 label
: OO
.ui
.msg( 'ooui-field-help' ),
12237 invisibleLabel
: true
12239 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
12240 helpWidget
.getPopup().$body
.html( help
.toString() );
12242 helpWidget
.getPopup().$body
.text( help
);
12245 helpId
= helpWidget
.getPopup().getBodyId();
12248 // Set the 'aria-describedby' attribute on the fieldWidget
12249 // Preference given to an input or a button
12251 this.fieldWidget
.$input
||
12252 this.fieldWidget
.$button
||
12253 this.fieldWidget
.$element
12254 ).attr( 'aria-describedby', helpId
);
12256 return helpWidget
.$element
;
12260 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget,
12261 * a button, and an optional label and/or help text. The field-widget (e.g., a
12262 * {@link OO.ui.TextInputWidget TextInputWidget}), is required and is specified before any optional
12263 * configuration settings.
12265 * Labels can be aligned in one of four ways:
12267 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12268 * A left-alignment is used for forms with many fields.
12269 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12270 * A right-alignment is used for long but familiar forms which users tab through,
12271 * verifying the current field with a quick glance at the label.
12272 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12273 * that users fill out from top to bottom.
12274 * - **inline**: The label is placed after the field-widget and aligned to the left.
12275 * An inline-alignment is best used with checkboxes or radio buttons.
12277 * Help text is accessed via a help icon that appears in the upper right corner of the rendered
12278 * field layout when help text is specified.
12281 * // Example of an ActionFieldLayout
12282 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12283 * new OO.ui.TextInputWidget( {
12284 * placeholder: 'Field widget'
12286 * new OO.ui.ButtonWidget( {
12290 * label: 'An ActionFieldLayout. This label is aligned top',
12292 * help: 'This is help text'
12296 * $( document.body ).append( actionFieldLayout.$element );
12299 * @extends OO.ui.FieldLayout
12302 * @param {OO.ui.Widget} fieldWidget Field widget
12303 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12304 * @param {Object} config
12306 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12307 // Allow passing positional parameters inside the config object
12308 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12309 config
= fieldWidget
;
12310 fieldWidget
= config
.fieldWidget
;
12311 buttonWidget
= config
.buttonWidget
;
12314 // Parent constructor
12315 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12318 this.buttonWidget
= buttonWidget
;
12319 this.$button
= $( '<span>' );
12320 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12323 this.$element
.addClass( 'oo-ui-actionFieldLayout' );
12325 .addClass( 'oo-ui-actionFieldLayout-button' )
12326 .append( this.buttonWidget
.$element
);
12328 .addClass( 'oo-ui-actionFieldLayout-input' )
12329 .append( this.fieldWidget
.$element
);
12330 this.$field
.append( this.$input
, this.$button
);
12335 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12338 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12339 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12340 * configured with a label as well. For more information and examples,
12341 * please see the [OOUI documentation on MediaWiki][1].
12344 * // Example of a fieldset layout
12345 * var input1 = new OO.ui.TextInputWidget( {
12346 * placeholder: 'A text input field'
12349 * var input2 = new OO.ui.TextInputWidget( {
12350 * placeholder: 'A text input field'
12353 * var fieldset = new OO.ui.FieldsetLayout( {
12354 * label: 'Example of a fieldset layout'
12357 * fieldset.addItems( [
12358 * new OO.ui.FieldLayout( input1, {
12359 * label: 'Field One'
12361 * new OO.ui.FieldLayout( input2, {
12362 * label: 'Field Two'
12365 * $( document.body ).append( fieldset.$element );
12367 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12370 * @extends OO.ui.Layout
12371 * @mixins OO.ui.mixin.IconElement
12372 * @mixins OO.ui.mixin.LabelElement
12373 * @mixins OO.ui.mixin.GroupElement
12376 * @param {Object} [config] Configuration options
12377 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset.
12378 * See OO.ui.FieldLayout for more information about fields.
12379 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon
12380 * will appear in the upper-right corner of the rendered field; clicking it will display the text
12381 * in a popup. For important messages, you are advised to use `notices`, as they are always shown.
12382 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12383 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12385 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12386 // Configuration initialization
12387 config
= config
|| {};
12389 // Parent constructor
12390 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12392 // Mixin constructors
12393 OO
.ui
.mixin
.IconElement
.call( this, config
);
12394 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12395 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12398 this.$header
= $( '<legend>' );
12399 if ( config
.help
) {
12400 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
12401 $overlay
: config
.$overlay
,
12405 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12408 label
: OO
.ui
.msg( 'ooui-field-help' ),
12409 invisibleLabel
: true
12411 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12412 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
12414 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
12416 this.$help
= this.popupButtonWidget
.$element
;
12418 this.$help
= $( [] );
12423 .addClass( 'oo-ui-fieldsetLayout-header' )
12424 .append( this.$icon
, this.$label
, this.$help
);
12425 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12427 .addClass( 'oo-ui-fieldsetLayout' )
12428 .prepend( this.$header
, this.$group
);
12429 if ( Array
.isArray( config
.items
) ) {
12430 this.addItems( config
.items
);
12436 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12437 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12438 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12439 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12441 /* Static Properties */
12447 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12450 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use
12451 * browser-based form submission for the fields instead of handling them in JavaScript. Form layouts
12452 * can be configured with an HTML form action, an encoding type, and a method using the #action,
12453 * #enctype, and #method configs, respectively.
12454 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12456 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12457 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12458 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12459 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12460 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12461 * often have simplified APIs to match the capabilities of HTML forms.
12462 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12464 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12465 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12468 * // Example of a form layout that wraps a fieldset layout.
12469 * var input1 = new OO.ui.TextInputWidget( {
12470 * placeholder: 'Username'
12472 * input2 = new OO.ui.TextInputWidget( {
12473 * placeholder: 'Password',
12476 * submit = new OO.ui.ButtonInputWidget( {
12479 * fieldset = new OO.ui.FieldsetLayout( {
12480 * label: 'A form layout'
12483 * fieldset.addItems( [
12484 * new OO.ui.FieldLayout( input1, {
12485 * label: 'Username',
12488 * new OO.ui.FieldLayout( input2, {
12489 * label: 'Password',
12492 * new OO.ui.FieldLayout( submit )
12494 * var form = new OO.ui.FormLayout( {
12495 * items: [ fieldset ],
12496 * action: '/api/formhandler',
12499 * $( document.body ).append( form.$element );
12502 * @extends OO.ui.Layout
12503 * @mixins OO.ui.mixin.GroupElement
12506 * @param {Object} [config] Configuration options
12507 * @cfg {string} [method] HTML form `method` attribute
12508 * @cfg {string} [action] HTML form `action` attribute
12509 * @cfg {string} [enctype] HTML form `enctype` attribute
12510 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12512 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12515 // Configuration initialization
12516 config
= config
|| {};
12518 // Parent constructor
12519 OO
.ui
.FormLayout
.parent
.call( this, config
);
12521 // Mixin constructors
12522 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12525 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12527 // Make sure the action is safe
12528 action
= config
.action
;
12529 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12530 action
= './' + action
;
12535 .addClass( 'oo-ui-formLayout' )
12537 method
: config
.method
,
12539 enctype
: config
.enctype
12541 if ( Array
.isArray( config
.items
) ) {
12542 this.addItems( config
.items
);
12548 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12549 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12554 * A 'submit' event is emitted when the form is submitted.
12559 /* Static Properties */
12565 OO
.ui
.FormLayout
.static.tagName
= 'form';
12570 * Handle form submit events.
12573 * @param {jQuery.Event} e Submit event
12575 * @return {OO.ui.FormLayout} The layout, for chaining
12577 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12578 if ( this.emit( 'submit' ) ) {
12584 * PanelLayouts expand to cover the entire area of their parent. They can be configured with
12585 * scrolling, padding, and a frame, and are often used together with
12586 * {@link OO.ui.StackLayout StackLayouts}.
12589 * // Example of a panel layout
12590 * var panel = new OO.ui.PanelLayout( {
12594 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12596 * $( document.body ).append( panel.$element );
12599 * @extends OO.ui.Layout
12602 * @param {Object} [config] Configuration options
12603 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12604 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12605 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12606 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside
12609 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12610 // Configuration initialization
12611 config
= $.extend( {
12618 // Parent constructor
12619 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12622 this.$element
.addClass( 'oo-ui-panelLayout' );
12623 if ( config
.scrollable
) {
12624 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12626 if ( config
.padded
) {
12627 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12629 if ( config
.expanded
) {
12630 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12632 if ( config
.framed
) {
12633 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12639 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12641 /* Static Methods */
12646 OO
.ui
.PanelLayout
.static.reusePreInfuseDOM = function ( node
, config
) {
12647 config
= OO
.ui
.PanelLayout
.parent
.static.reusePreInfuseDOM( node
, config
);
12648 if ( config
.preserveContent
!== false ) {
12649 config
.$content
= $( node
).contents();
12657 * Focus the panel layout
12659 * The default implementation just focuses the first focusable element in the panel
12661 OO
.ui
.PanelLayout
.prototype.focus = function () {
12662 OO
.ui
.findFocusable( this.$element
).focus();
12666 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12667 * items), with small margins between them. Convenient when you need to put a number of block-level
12668 * widgets on a single line next to each other.
12670 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12673 * // HorizontalLayout with a text input and a label.
12674 * var layout = new OO.ui.HorizontalLayout( {
12676 * new OO.ui.LabelWidget( { label: 'Label' } ),
12677 * new OO.ui.TextInputWidget( { value: 'Text' } )
12680 * $( document.body ).append( layout.$element );
12683 * @extends OO.ui.Layout
12684 * @mixins OO.ui.mixin.GroupElement
12687 * @param {Object} [config] Configuration options
12688 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12690 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12691 // Configuration initialization
12692 config
= config
|| {};
12694 // Parent constructor
12695 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12697 // Mixin constructors
12698 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12701 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12702 if ( Array
.isArray( config
.items
) ) {
12703 this.addItems( config
.items
);
12709 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12710 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12713 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12714 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12715 * (to adjust the value in increments) to allow the user to enter a number.
12718 * // A NumberInputWidget.
12719 * var numberInput = new OO.ui.NumberInputWidget( {
12720 * label: 'NumberInputWidget',
12721 * input: { value: 5 },
12725 * $( document.body ).append( numberInput.$element );
12728 * @extends OO.ui.TextInputWidget
12731 * @param {Object} [config] Configuration options
12732 * @cfg {Object} [minusButton] Configuration options to pass to the
12733 * {@link OO.ui.ButtonWidget decrementing button widget}.
12734 * @cfg {Object} [plusButton] Configuration options to pass to the
12735 * {@link OO.ui.ButtonWidget incrementing button widget}.
12736 * @cfg {number} [min=-Infinity] Minimum allowed value
12737 * @cfg {number} [max=Infinity] Maximum allowed value
12738 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12739 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or Up/Down arrow keys.
12740 * Defaults to `step` if specified, otherwise `1`.
12741 * @cfg {number} [pageStep=10*buttonStep] Delta when using the Page-up/Page-down keys.
12742 * Defaults to 10 times `buttonStep`.
12743 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12745 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12746 var $field
= $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' );
12748 // Configuration initialization
12749 config
= $.extend( {
12755 // For backward compatibility
12756 $.extend( config
, config
.input
);
12759 // Parent constructor
12760 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12764 if ( config
.showButtons
) {
12765 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12767 disabled
: this.isDisabled(),
12769 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12774 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12775 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12777 disabled
: this.isDisabled(),
12779 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12784 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12789 keydown
: this.onKeyDown
.bind( this ),
12790 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12792 if ( config
.showButtons
) {
12793 this.plusButton
.connect( this, {
12794 click
: [ 'onButtonClick', +1 ]
12796 this.minusButton
.connect( this, {
12797 click
: [ 'onButtonClick', -1 ]
12802 $field
.append( this.$input
);
12803 if ( config
.showButtons
) {
12805 .prepend( this.minusButton
.$element
)
12806 .append( this.plusButton
.$element
);
12810 if ( config
.allowInteger
|| config
.isInteger
) {
12811 // Backward compatibility
12814 this.setRange( config
.min
, config
.max
);
12815 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
12816 // Set the validation method after we set step and range
12817 // so that it doesn't immediately call setValidityFlag
12818 this.setValidation( this.validateNumber
.bind( this ) );
12821 .addClass( 'oo-ui-numberInputWidget' )
12822 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12828 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12832 // Backward compatibility
12833 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12834 this.setStep( flag
? 1 : null );
12836 // Backward compatibility
12837 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12839 // Backward compatibility
12840 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12841 return this.step
=== 1;
12843 // Backward compatibility
12844 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12847 * Set the range of allowed values
12849 * @param {number} min Minimum allowed value
12850 * @param {number} max Maximum allowed value
12852 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12854 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12858 this.$input
.attr( 'min', this.min
);
12859 this.$input
.attr( 'max', this.max
);
12860 this.setValidityFlag();
12864 * Get the current range
12866 * @return {number[]} Minimum and maximum values
12868 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12869 return [ this.min
, this.max
];
12873 * Set the stepping deltas
12875 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12876 * Defaults to `step` if specified, otherwise `1`.
12877 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12878 * Defaults to 10 times `buttonStep`.
12879 * @param {number|null} [step] If specified, the field only accepts values that are multiples
12882 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
12883 if ( buttonStep
=== undefined ) {
12884 buttonStep
= step
|| 1;
12886 if ( pageStep
=== undefined ) {
12887 pageStep
= 10 * buttonStep
;
12889 if ( step
!== null && step
<= 0 ) {
12890 throw new Error( 'Step value, if given, must be positive' );
12892 if ( buttonStep
<= 0 ) {
12893 throw new Error( 'Button step value must be positive' );
12895 if ( pageStep
<= 0 ) {
12896 throw new Error( 'Page step value must be positive' );
12899 this.buttonStep
= buttonStep
;
12900 this.pageStep
= pageStep
;
12901 this.$input
.attr( 'step', this.step
|| 'any' );
12902 this.setValidityFlag();
12908 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12909 if ( value
=== '' ) {
12910 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12911 // so here we make sure an 'empty' value is actually displayed as such.
12912 this.$input
.val( '' );
12914 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12918 * Get the current stepping values
12920 * @return {number[]} Button step, page step, and validity step
12922 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12923 return [ this.buttonStep
, this.pageStep
, this.step
];
12927 * Get the current value of the widget as a number
12929 * @return {number} May be NaN, or an invalid number
12931 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12932 return +this.getValue();
12936 * Adjust the value of the widget
12938 * @param {number} delta Adjustment amount
12940 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12941 var n
, v
= this.getNumericValue();
12944 if ( isNaN( delta
) || !isFinite( delta
) ) {
12945 throw new Error( 'Delta must be a finite number' );
12948 if ( isNaN( v
) ) {
12952 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12954 n
= Math
.round( n
/ this.step
) * this.step
;
12959 this.setValue( n
);
12966 * @param {string} value Field value
12967 * @return {boolean}
12969 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12971 if ( value
=== '' ) {
12972 return !this.isRequired();
12975 if ( isNaN( n
) || !isFinite( n
) ) {
12979 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
12983 if ( n
< this.min
|| n
> this.max
) {
12991 * Handle mouse click events.
12994 * @param {number} dir +1 or -1
12996 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
12997 this.adjustValue( dir
* this.buttonStep
);
13001 * Handle mouse wheel events.
13004 * @param {jQuery.Event} event
13005 * @return {undefined/boolean} False to prevent default if event is handled
13007 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
13010 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
13011 // Standard 'wheel' event
13012 if ( event
.originalEvent
.deltaMode
!== undefined ) {
13013 this.sawWheelEvent
= true;
13015 if ( event
.originalEvent
.deltaY
) {
13016 delta
= -event
.originalEvent
.deltaY
;
13017 } else if ( event
.originalEvent
.deltaX
) {
13018 delta
= event
.originalEvent
.deltaX
;
13021 // Non-standard events
13022 if ( !this.sawWheelEvent
) {
13023 if ( event
.originalEvent
.wheelDeltaX
) {
13024 delta
= -event
.originalEvent
.wheelDeltaX
;
13025 } else if ( event
.originalEvent
.wheelDeltaY
) {
13026 delta
= event
.originalEvent
.wheelDeltaY
;
13027 } else if ( event
.originalEvent
.wheelDelta
) {
13028 delta
= event
.originalEvent
.wheelDelta
;
13029 } else if ( event
.originalEvent
.detail
) {
13030 delta
= -event
.originalEvent
.detail
;
13035 delta
= delta
< 0 ? -1 : 1;
13036 this.adjustValue( delta
* this.buttonStep
);
13044 * Handle key down events.
13047 * @param {jQuery.Event} e Key down event
13048 * @return {undefined/boolean} False to prevent default if event is handled
13050 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
13051 if ( !this.isDisabled() ) {
13052 switch ( e
.which
) {
13053 case OO
.ui
.Keys
.UP
:
13054 this.adjustValue( this.buttonStep
);
13056 case OO
.ui
.Keys
.DOWN
:
13057 this.adjustValue( -this.buttonStep
);
13059 case OO
.ui
.Keys
.PAGEUP
:
13060 this.adjustValue( this.pageStep
);
13062 case OO
.ui
.Keys
.PAGEDOWN
:
13063 this.adjustValue( -this.pageStep
);
13072 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
13074 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13076 if ( this.minusButton
) {
13077 this.minusButton
.setDisabled( this.isDisabled() );
13079 if ( this.plusButton
) {
13080 this.plusButton
.setDisabled( this.isDisabled() );
13087 * SelectFileInputWidgets allow for selecting files, using <input type="file">. These
13088 * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
13089 * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
13090 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
13092 * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename.
13095 * // A file select input widget.
13096 * var selectFile = new OO.ui.SelectFileInputWidget();
13097 * $( document.body ).append( selectFile.$element );
13099 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
13102 * @extends OO.ui.InputWidget
13105 * @param {Object} [config] Configuration options
13106 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
13107 * @cfg {boolean} [multiple=false] Allow multiple files to be selected.
13108 * @cfg {string} [placeholder] Text to display when no file is selected.
13109 * @cfg {Object} [button] Config to pass to select file button.
13110 * @cfg {string} [icon] Icon to show next to file info
13112 OO
.ui
.SelectFileInputWidget
= function OoUiSelectFileInputWidget( config
) {
13115 config
= config
|| {};
13117 // Construct buttons before parent method is called (calling setDisabled)
13118 this.selectButton
= new OO
.ui
.ButtonWidget( $.extend( {
13119 $element
: $( '<label>' ),
13120 classes
: [ 'oo-ui-selectFileInputWidget-selectButton' ],
13121 label
: OO
.ui
.msg( 'ooui-selectfile-button-select' )
13122 }, config
.button
) );
13124 // Configuration initialization
13125 config
= $.extend( {
13127 placeholder
: OO
.ui
.msg( 'ooui-selectfile-placeholder' ),
13128 $tabIndexed
: this.selectButton
.$tabIndexed
13131 this.info
= new OO
.ui
.SearchInputWidget( {
13132 classes
: [ 'oo-ui-selectFileInputWidget-info' ],
13133 placeholder
: config
.placeholder
,
13134 // Pass an empty collection so that .focus() always does nothing
13135 $tabIndexed
: $( [] )
13136 } ).setIcon( config
.icon
);
13137 // Set tabindex manually on $input as $tabIndexed has been overridden
13138 this.info
.$input
.attr( 'tabindex', -1 );
13140 // Parent constructor
13141 OO
.ui
.SelectFileInputWidget
.parent
.call( this, config
);
13144 this.currentFiles
= this.filterFiles( this.$input
[ 0 ].files
|| [] );
13145 if ( Array
.isArray( config
.accept
) ) {
13146 this.accept
= config
.accept
;
13148 this.accept
= null;
13150 this.multiple
= !!config
.multiple
;
13153 this.info
.connect( this, { change
: 'onInfoChange' } );
13154 this.selectButton
.$button
.on( {
13155 keypress
: this.onKeyPress
.bind( this )
13158 change
: this.onFileSelected
.bind( this ),
13160 // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
13161 // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
13162 // Since this messes with our custom styling (the file input has large dimensions and this
13163 // causes the label to scroll out of view), scroll the button back to top. (T192131)
13164 focus: function () {
13165 widget
.$input
.parent().prop( 'scrollTop', 0 );
13168 this.connect( this, { change
: 'updateUI' } );
13170 this.fieldLayout
= new OO
.ui
.ActionFieldLayout( this.info
, this.selectButton
, { align
: 'top' } );
13175 // this.selectButton is tabindexed
13177 // Infused input may have previously by
13178 // TabIndexed, so remove aria-disabled attr.
13179 'aria-disabled': null
13182 if ( this.accept
) {
13183 this.$input
.attr( 'accept', this.accept
.join( ', ' ) );
13185 if ( this.multiple
) {
13186 this.$input
.attr( 'multiple', '' );
13188 this.selectButton
.$button
.append( this.$input
);
13191 .addClass( 'oo-ui-selectFileInputWidget' )
13192 .append( this.fieldLayout
.$element
);
13199 OO
.inheritClass( OO
.ui
.SelectFileInputWidget
, OO
.ui
.InputWidget
);
13201 /* Static properties */
13203 // Set empty title so that browser default tooltips like "No file chosen" don't appear.
13204 // On SelectFileWidget this tooltip will often be incorrect, so create a consistent
13205 // experience on SelectFileInputWidget.
13206 OO
.ui
.SelectFileInputWidget
.static.title
= '';
13211 * Get the filename of the currently selected file.
13213 * @return {string} Filename
13215 OO
.ui
.SelectFileInputWidget
.prototype.getFilename = function () {
13216 if ( this.currentFiles
.length
) {
13217 return this.currentFiles
.map( function ( file
) {
13221 // Try to strip leading fakepath.
13222 return this.getValue().split( '\\' ).pop();
13229 OO
.ui
.SelectFileInputWidget
.prototype.setValue = function ( value
) {
13230 if ( value
=== undefined ) {
13231 // Called during init, don't replace value if just infusing.
13235 // We need to update this.value, but without trying to modify
13236 // the DOM value, which would throw an exception.
13237 if ( this.value
!== value
) {
13238 this.value
= value
;
13239 this.emit( 'change', this.value
);
13242 this.currentFiles
= [];
13244 OO
.ui
.SelectFileInputWidget
.super.prototype.setValue
.call( this, '' );
13249 * Handle file selection from the input.
13252 * @param {jQuery.Event} e
13254 OO
.ui
.SelectFileInputWidget
.prototype.onFileSelected = function ( e
) {
13255 this.currentFiles
= this.filterFiles( e
.target
.files
|| [] );
13259 * Update the user interface when a file is selected or unselected.
13263 OO
.ui
.SelectFileInputWidget
.prototype.updateUI = function () {
13264 this.info
.setValue( this.getFilename() );
13268 * Determine if we should accept this file.
13271 * @param {FileList|File[]} files Files to filter
13272 * @return {File[]} Filter files
13274 OO
.ui
.SelectFileInputWidget
.prototype.filterFiles = function ( files
) {
13275 var accept
= this.accept
;
13277 function mimeAllowed( file
) {
13279 mimeType
= file
.type
;
13281 if ( !accept
|| !mimeType
) {
13285 for ( i
= 0; i
< accept
.length
; i
++ ) {
13286 mimeTest
= accept
[ i
];
13287 if ( mimeTest
=== mimeType
) {
13289 } else if ( mimeTest
.substr( -2 ) === '/*' ) {
13290 mimeTest
= mimeTest
.substr( 0, mimeTest
.length
- 1 );
13291 if ( mimeType
.substr( 0, mimeTest
.length
) === mimeTest
) {
13299 return Array
.prototype.filter
.call( files
, mimeAllowed
);
13303 * Handle info input change events
13305 * The info widget can only be changed by the user
13306 * with the clear button.
13309 * @param {string} value
13311 OO
.ui
.SelectFileInputWidget
.prototype.onInfoChange = function ( value
) {
13312 if ( value
=== '' ) {
13313 this.setValue( null );
13318 * Handle key press events.
13321 * @param {jQuery.Event} e Key press event
13322 * @return {undefined/boolean} False to prevent default if event is handled
13324 OO
.ui
.SelectFileInputWidget
.prototype.onKeyPress = function ( e
) {
13325 if ( !this.isDisabled() && this.$input
&&
13326 ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
13328 // Emit a click to open the file selector.
13329 this.$input
.trigger( 'click' );
13330 // Taking focus from the selectButton means keyUp isn't fired, so fire it manually.
13331 this.selectButton
.onDocumentKeyUp( e
);
13339 OO
.ui
.SelectFileInputWidget
.prototype.setDisabled = function ( disabled
) {
13341 OO
.ui
.SelectFileInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13343 this.selectButton
.setDisabled( disabled
);
13344 this.info
.setDisabled( disabled
);
13351 //# sourceMappingURL=oojs-ui-core.js.map.json