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-07-23T03:23:32Z
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
,
295 previous
= Date
.now() - wait
,
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
= Math
.max( wait
- ( Date
.now() - previous
), 0 );
311 // If time is up, do setTimeout( run, 0 ) so the function
312 // always runs asynchronously, just like Promise#then .
313 timeout
= setTimeout( run
, remaining
);
319 * Reconstitute a JavaScript object corresponding to a widget created by
320 * the PHP implementation.
322 * This is an alias for `OO.ui.Element.static.infuse()`.
324 * @param {string|HTMLElement|jQuery} idOrNode
325 * A DOM id (if a string) or node for the widget to infuse.
326 * @param {Object} [config] Configuration options
327 * @return {OO.ui.Element}
328 * The `OO.ui.Element` corresponding to this (infusable) document node.
330 OO
.ui
.infuse = function ( idOrNode
, config
) {
331 return OO
.ui
.Element
.static.infuse( idOrNode
, config
);
335 * Get a localized message.
337 * After the message key, message parameters may optionally be passed. In the default
338 * implementation, any occurrences of $1 are replaced with the first parameter, $2 with the
339 * second parameter, etc.
340 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long
341 * as they support unnamed, ordered message parameters.
343 * In environments that provide a localization system, this function should be overridden to
344 * return the message translated in the user's language. The default implementation always
345 * returns English messages. An example of doing this with
346 * [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) follows.
349 * var i, iLen, button,
350 * messagePath = 'oojs-ui/dist/i18n/',
351 * languages = [ $.i18n().locale, 'ur', 'en' ],
354 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
355 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
358 * $.i18n().load( languageMap ).done( function() {
359 * // Replace the built-in `msg` only once we've loaded the internationalization.
360 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
361 * // you put off creating any widgets until this promise is complete, no English
362 * // will be displayed.
363 * OO.ui.msg = $.i18n;
365 * // A button displaying "OK" in the default locale
366 * button = new OO.ui.ButtonWidget( {
367 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
370 * $( document.body ).append( button.$element );
372 * // A button displaying "OK" in Urdu
373 * $.i18n().locale = 'ur';
374 * button = new OO.ui.ButtonWidget( {
375 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
378 * $( document.body ).append( button.$element );
381 * @param {string} key Message key
382 * @param {...Mixed} [params] Message parameters
383 * @return {string} Translated message with parameters substituted
385 OO
.ui
.msg = function ( key
) {
386 // `OO.ui.msg.messages` is defined in code generated during the build process
387 var messages
= OO
.ui
.msg
.messages
,
388 message
= messages
[ key
],
389 params
= Array
.prototype.slice
.call( arguments
, 1 );
390 if ( typeof message
=== 'string' ) {
391 // Perform $1 substitution
392 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
393 var i
= parseInt( n
, 10 );
394 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
397 // Return placeholder if message not found
398 message
= '[' + key
+ ']';
404 * Package a message and arguments for deferred resolution.
406 * Use this when you are statically specifying a message and the message may not yet be present.
408 * @param {string} key Message key
409 * @param {...Mixed} [params] Message parameters
410 * @return {Function} Function that returns the resolved message when executed
412 OO
.ui
.deferMsg = function () {
413 var args
= arguments
;
415 return OO
.ui
.msg
.apply( OO
.ui
, args
);
422 * If the message is a function it will be executed, otherwise it will pass through directly.
424 * @param {Function|string} msg Deferred message, or message text
425 * @return {string} Resolved message
427 OO
.ui
.resolveMsg = function ( msg
) {
428 if ( typeof msg
=== 'function' ) {
435 * @param {string} url
438 OO
.ui
.isSafeUrl = function ( url
) {
439 // Keep this function in sync with php/Tag.php
440 var i
, protocolWhitelist
;
442 function stringStartsWith( haystack
, needle
) {
443 return haystack
.substr( 0, needle
.length
) === needle
;
446 protocolWhitelist
= [
447 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
448 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
449 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
456 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
457 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
462 // This matches '//' too
463 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
466 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
474 * Check if the user has a 'mobile' device.
476 * For our purposes this means the user is primarily using an
477 * on-screen keyboard, touch input instead of a mouse and may
478 * have a physically small display.
480 * It is left up to implementors to decide how to compute this
481 * so the default implementation always returns false.
483 * @return {boolean} User is on a mobile device
485 OO
.ui
.isMobile = function () {
490 * Get the additional spacing that should be taken into account when displaying elements that are
491 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
492 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
494 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
495 * the extra spacing from that edge of viewport (in pixels)
497 OO
.ui
.getViewportSpacing = function () {
507 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
508 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
510 * @return {jQuery} Default overlay node
512 OO
.ui
.getDefaultOverlay = function () {
513 if ( !OO
.ui
.$defaultOverlay
) {
514 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
515 $( document
.body
).append( OO
.ui
.$defaultOverlay
);
517 return OO
.ui
.$defaultOverlay
;
521 * Message store for the default implementation of OO.ui.msg.
523 * Environments that provide a localization system should not use this, but should override
524 * OO.ui.msg altogether.
528 OO
.ui
.msg
.messages
= {
529 "ooui-outline-control-move-down": "Move item down",
530 "ooui-outline-control-move-up": "Move item up",
531 "ooui-outline-control-remove": "Remove item",
532 "ooui-toolbar-more": "More",
533 "ooui-toolgroup-expand": "More",
534 "ooui-toolgroup-collapse": "Fewer",
535 "ooui-item-remove": "Remove",
536 "ooui-dialog-message-accept": "OK",
537 "ooui-dialog-message-reject": "Cancel",
538 "ooui-dialog-process-error": "Something went wrong",
539 "ooui-dialog-process-dismiss": "Dismiss",
540 "ooui-dialog-process-retry": "Try again",
541 "ooui-dialog-process-continue": "Continue",
542 "ooui-combobox-button-label": "Dropdown for combobox",
543 "ooui-selectfile-button-select": "Select a file",
544 "ooui-selectfile-not-supported": "File selection is not supported",
545 "ooui-selectfile-placeholder": "No file is selected",
546 "ooui-selectfile-dragdrop-placeholder": "Drop file here",
547 "ooui-field-help": "Help"
555 * Namespace for OOUI mixins.
557 * Mixins are named according to the type of object they are intended to
558 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
559 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
560 * is intended to be mixed in to an instance of OO.ui.Widget.
568 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
569 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not
570 * have events connected to them and can't be interacted with.
576 * @param {Object} [config] Configuration options
577 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are
578 * added to the top level (e.g., the outermost div) of the element. See the
579 * [OOUI documentation on MediaWiki][2] for an example.
580 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
581 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
582 * @cfg {string} [text] Text to insert
583 * @cfg {Array} [content] An array of content elements to append (after #text).
584 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
585 * Instances of OO.ui.Element will have their $element appended.
586 * @cfg {jQuery} [$content] Content elements to append (after #text).
587 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
588 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number,
590 * Data can also be specified with the #setData method.
592 OO
.ui
.Element
= function OoUiElement( config
) {
593 if ( OO
.ui
.isDemo
) {
594 this.initialConfig
= config
;
596 // Configuration initialization
597 config
= config
|| {};
600 this.elementId
= null;
602 this.data
= config
.data
;
603 this.$element
= config
.$element
||
604 $( document
.createElement( this.getTagName() ) );
605 this.elementGroup
= null;
608 if ( Array
.isArray( config
.classes
) ) {
609 this.$element
.addClass( config
.classes
);
612 this.setElementId( config
.id
);
615 this.$element
.text( config
.text
);
617 if ( config
.content
) {
618 // The `content` property treats plain strings as text; use an
619 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
620 // appropriate $element appended.
621 this.$element
.append( config
.content
.map( function ( v
) {
622 if ( typeof v
=== 'string' ) {
623 // Escape string so it is properly represented in HTML.
624 // Don't create empty text nodes for empty strings.
625 return v
? document
.createTextNode( v
) : undefined;
626 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
629 } else if ( v
instanceof OO
.ui
.Element
) {
635 if ( config
.$content
) {
636 // The `$content` property treats plain strings as HTML.
637 this.$element
.append( config
.$content
);
643 OO
.initClass( OO
.ui
.Element
);
645 /* Static Properties */
648 * The name of the HTML tag used by the element.
650 * The static value may be ignored if the #getTagName method is overridden.
656 OO
.ui
.Element
.static.tagName
= 'div';
661 * Reconstitute a JavaScript object corresponding to a widget created
662 * by the PHP implementation.
664 * @param {string|HTMLElement|jQuery} idOrNode
665 * A DOM id (if a string) or node for the widget to infuse.
666 * @param {Object} [config] Configuration options
667 * @return {OO.ui.Element}
668 * The `OO.ui.Element` corresponding to this (infusable) document node.
669 * For `Tag` objects emitted on the HTML side (used occasionally for content)
670 * the value returned is a newly-created Element wrapping around the existing
673 OO
.ui
.Element
.static.infuse = function ( idOrNode
, config
) {
674 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, config
, false );
676 if ( typeof idOrNode
=== 'string' ) {
677 // IDs deprecated since 0.29.7
678 OO
.ui
.warnDeprecation(
679 'Passing a string ID to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
682 // Verify that the type matches up.
683 // FIXME: uncomment after T89721 is fixed, see T90929.
685 if ( !( obj instanceof this['class'] ) ) {
686 throw new Error( 'Infusion type mismatch!' );
693 * Implementation helper for `infuse`; skips the type check and has an
694 * extra property so that only the top-level invocation touches the DOM.
697 * @param {string|HTMLElement|jQuery} idOrNode
698 * @param {Object} [config] Configuration options
699 * @param {jQuery.Promise} [domPromise] A promise that will be resolved
700 * when the top-level widget of this infusion is inserted into DOM,
701 * replacing the original node; only used internally.
702 * @return {OO.ui.Element}
704 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, config
, domPromise
) {
705 // look for a cached result of a previous infusion.
706 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
707 if ( typeof idOrNode
=== 'string' ) {
709 $elem
= $( document
.getElementById( id
) );
711 $elem
= $( idOrNode
);
712 id
= $elem
.attr( 'id' );
714 if ( !$elem
.length
) {
715 if ( typeof idOrNode
=== 'string' ) {
716 error
= 'Widget not found: ' + idOrNode
;
717 } else if ( idOrNode
&& idOrNode
.selector
) {
718 error
= 'Widget not found: ' + idOrNode
.selector
;
720 error
= 'Widget not found';
722 throw new Error( error
);
724 if ( $elem
[ 0 ].oouiInfused
) {
725 $elem
= $elem
[ 0 ].oouiInfused
;
727 data
= $elem
.data( 'ooui-infused' );
730 if ( data
=== true ) {
731 throw new Error( 'Circular dependency! ' + id
);
734 // Pick up dynamic state, like focus, value of form inputs, scroll position, etc.
735 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
736 // Restore dynamic state after the new element is re-inserted into DOM under
738 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
739 infusedChildren
= $elem
.data( 'ooui-infused-children' );
740 if ( infusedChildren
&& infusedChildren
.length
) {
741 infusedChildren
.forEach( function ( data
) {
742 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
743 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
749 data
= $elem
.attr( 'data-ooui' );
751 throw new Error( 'No infusion data found: ' + id
);
754 data
= JSON
.parse( data
);
758 if ( !( data
&& data
._
) ) {
759 throw new Error( 'No valid infusion data found: ' + id
);
761 if ( data
._
=== 'Tag' ) {
762 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
763 return new OO
.ui
.Element( $.extend( {}, config
, { $element
: $elem
} ) );
765 parts
= data
._
.split( '.' );
766 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
767 if ( cls
=== undefined ) {
768 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
771 // Verify that we're creating an OO.ui.Element instance
774 while ( parent
!== undefined ) {
775 if ( parent
=== OO
.ui
.Element
) {
780 parent
= parent
.parent
;
783 if ( parent
!== OO
.ui
.Element
) {
784 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
789 domPromise
= top
.promise();
791 $elem
.data( 'ooui-infused', true ); // prevent loops
792 data
.id
= id
; // implicit
793 infusedChildren
= [];
794 data
= OO
.copy( data
, null, function deserialize( value
) {
796 if ( OO
.isPlainObject( value
) ) {
798 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, config
, domPromise
);
799 infusedChildren
.push( infused
);
800 // Flatten the structure
801 infusedChildren
.push
.apply(
803 infused
.$element
.data( 'ooui-infused-children' ) || []
805 infused
.$element
.removeData( 'ooui-infused-children' );
808 if ( value
.html
!== undefined ) {
809 return new OO
.ui
.HtmlSnippet( value
.html
);
813 // allow widgets to reuse parts of the DOM
814 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
815 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
816 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
818 // eslint-disable-next-line new-cap
819 obj
= new cls( $.extend( {}, config
, data
) );
820 // If anyone is holding a reference to the old DOM element,
821 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
822 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
823 $elem
[ 0 ].oouiInfused
= obj
.$element
;
824 // now replace old DOM with this new DOM.
826 // An efficient constructor might be able to reuse the entire DOM tree of the original
827 // element, so only mutate the DOM if we need to.
828 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
829 $elem
.replaceWith( obj
.$element
);
833 obj
.$element
.data( 'ooui-infused', obj
);
834 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
835 // set the 'data-ooui' attribute so we can identify infused widgets
836 obj
.$element
.attr( 'data-ooui', '' );
837 // restore dynamic state after the new element is inserted into DOM
838 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
843 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
845 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
846 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
847 * constructor, which will be given the enhanced config.
850 * @param {HTMLElement} node
851 * @param {Object} config
854 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
859 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM
860 * node (and its children) that represent an Element of the same class and the given configuration,
861 * generated by the PHP implementation.
863 * This method is called just before `node` is detached from the DOM. The return value of this
864 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
865 * is inserted into DOM to replace `node`.
868 * @param {HTMLElement} node
869 * @param {Object} config
872 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
877 * Get the document of an element.
880 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
881 * @return {HTMLDocument|null} Document object
883 OO
.ui
.Element
.static.getDocument = function ( obj
) {
884 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
885 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
886 // Empty jQuery selections might have a context
893 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
898 * Get the window of an element or document.
901 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
902 * @return {Window} Window object
904 OO
.ui
.Element
.static.getWindow = function ( obj
) {
905 var doc
= this.getDocument( obj
);
906 return doc
.defaultView
;
910 * Get the direction of an element or document.
913 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
914 * @return {string} Text direction, either 'ltr' or 'rtl'
916 OO
.ui
.Element
.static.getDir = function ( obj
) {
919 if ( obj
instanceof $ ) {
922 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
923 isWin
= obj
.document
!== undefined;
924 if ( isDoc
|| isWin
) {
930 return $( obj
).css( 'direction' );
934 * Get the offset between two frames.
936 * TODO: Make this function not use recursion.
939 * @param {Window} from Window of the child frame
940 * @param {Window} [to=window] Window of the parent frame
941 * @param {Object} [offset] Offset to start with, used internally
942 * @return {Object} Offset object, containing left and top properties
944 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
945 var i
, len
, frames
, frame
, rect
;
951 offset
= { top
: 0, left
: 0 };
953 if ( from.parent
=== from ) {
957 // Get iframe element
958 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
959 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
960 if ( frames
[ i
].contentWindow
=== from ) {
966 // Recursively accumulate offset values
968 rect
= frame
.getBoundingClientRect();
969 offset
.left
+= rect
.left
;
970 offset
.top
+= rect
.top
;
972 this.getFrameOffset( from.parent
, offset
);
979 * Get the offset between two elements.
981 * The two elements may be in a different frame, but in that case the frame $element is in must
982 * be contained in the frame $anchor is in.
985 * @param {jQuery} $element Element whose position to get
986 * @param {jQuery} $anchor Element to get $element's position relative to
987 * @return {Object} Translated position coordinates, containing top and left properties
989 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
990 var iframe
, iframePos
,
991 pos
= $element
.offset(),
992 anchorPos
= $anchor
.offset(),
993 elementDocument
= this.getDocument( $element
),
994 anchorDocument
= this.getDocument( $anchor
);
996 // If $element isn't in the same document as $anchor, traverse up
997 while ( elementDocument
!== anchorDocument
) {
998 iframe
= elementDocument
.defaultView
.frameElement
;
1000 throw new Error( '$element frame is not contained in $anchor frame' );
1002 iframePos
= $( iframe
).offset();
1003 pos
.left
+= iframePos
.left
;
1004 pos
.top
+= iframePos
.top
;
1005 elementDocument
= iframe
.ownerDocument
;
1007 pos
.left
-= anchorPos
.left
;
1008 pos
.top
-= anchorPos
.top
;
1013 * Get element border sizes.
1016 * @param {HTMLElement} el Element to measure
1017 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1019 OO
.ui
.Element
.static.getBorders = function ( el
) {
1020 var doc
= el
.ownerDocument
,
1021 win
= doc
.defaultView
,
1022 style
= win
.getComputedStyle( el
, null ),
1024 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1025 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1026 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1027 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1038 * Get dimensions of an element or window.
1041 * @param {HTMLElement|Window} el Element to measure
1042 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1044 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1046 doc
= el
.ownerDocument
|| el
.document
,
1047 win
= doc
.defaultView
;
1049 if ( win
=== el
|| el
=== doc
.documentElement
) {
1052 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1054 top
: $win
.scrollTop(),
1055 left
: OO
.ui
.Element
.static.getScrollLeft( win
)
1057 scrollbar
: { right
: 0, bottom
: 0 },
1061 bottom
: $win
.innerHeight(),
1062 right
: $win
.innerWidth()
1068 borders
: this.getBorders( el
),
1070 top
: $el
.scrollTop(),
1071 left
: OO
.ui
.Element
.static.getScrollLeft( el
)
1074 right
: $el
.innerWidth() - el
.clientWidth
,
1075 bottom
: $el
.innerHeight() - el
.clientHeight
1077 rect
: el
.getBoundingClientRect()
1083 var rtlScrollType
= null;
1085 // Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1086 // Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1087 function rtlScrollTypeTest() {
1088 var $definer
= $( '<div>' ).attr( {
1090 style
: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
1092 definer
= $definer
[ 0 ];
1094 $definer
.appendTo( 'body' );
1095 if ( definer
.scrollLeft
> 0 ) {
1097 rtlScrollType
= 'default';
1099 definer
.scrollLeft
= 1;
1100 if ( definer
.scrollLeft
=== 0 ) {
1101 // Firefox, old Opera
1102 rtlScrollType
= 'negative';
1104 // Internet Explorer, Edge
1105 rtlScrollType
= 'reverse';
1111 function isRoot( el
) {
1112 return el
.window
=== el
||
1113 el
=== el
.ownerDocument
.body
||
1114 el
=== el
.ownerDocument
.documentElement
;
1118 * Convert native `scrollLeft` value to a value consistent between browsers. See #getScrollLeft.
1119 * @param {number} nativeOffset Native `scrollLeft` value
1120 * @param {HTMLElement|Window} el Element from which the value was obtained
1123 OO
.ui
.Element
.static.computeNormalizedScrollLeft = function ( nativeOffset
, el
) {
1124 // All browsers use the correct scroll type ('negative') on the root, so don't
1125 // do any fixups when looking at the root element
1126 var direction
= isRoot( el
) ? 'ltr' : $( el
).css( 'direction' );
1128 if ( direction
=== 'rtl' ) {
1129 if ( rtlScrollType
=== null ) {
1130 rtlScrollTypeTest();
1132 if ( rtlScrollType
=== 'reverse' ) {
1133 return -nativeOffset
;
1134 } else if ( rtlScrollType
=== 'default' ) {
1135 return nativeOffset
- el
.scrollWidth
+ el
.clientWidth
;
1139 return nativeOffset
;
1143 * Convert our normalized `scrollLeft` value to a value for current browser. See #getScrollLeft.
1144 * @param {number} normalizedOffset Normalized `scrollLeft` value
1145 * @param {HTMLElement|Window} el Element on which the value will be set
1148 OO
.ui
.Element
.static.computeNativeScrollLeft = function ( normalizedOffset
, el
) {
1149 // All browsers use the correct scroll type ('negative') on the root, so don't
1150 // do any fixups when looking at the root element
1151 var direction
= isRoot( el
) ? 'ltr' : $( el
).css( 'direction' );
1153 if ( direction
=== 'rtl' ) {
1154 if ( rtlScrollType
=== null ) {
1155 rtlScrollTypeTest();
1157 if ( rtlScrollType
=== 'reverse' ) {
1158 return -normalizedOffset
;
1159 } else if ( rtlScrollType
=== 'default' ) {
1160 return normalizedOffset
+ el
.scrollWidth
- el
.clientWidth
;
1164 return normalizedOffset
;
1168 * Get the number of pixels that an element's content is scrolled to the left.
1170 * This function smooths out browser inconsistencies (nicely described in the README at
1171 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1172 * with Firefox's 'scrollLeft', which seems the sanest.
1174 * (Firefox's scrollLeft handling is nice because it increases from left to right, consistently
1175 * with `getBoundingClientRect().left` and related APIs; because initial value is zero, so
1176 * resetting it is easy; because adapting a hardcoded scroll position to a symmetrical RTL
1177 * interface requires just negating it, rather than involving `clientWidth` and `scrollWidth`;
1178 * and because if you mess up and don't adapt your code to RTL, it will scroll to the beginning
1179 * rather than somewhere randomly in the middle but not where you wanted.)
1183 * @param {HTMLElement|Window} el Element to measure
1184 * @return {number} Scroll position from the left.
1185 * If the element's direction is LTR, this is a positive number between `0` (initial scroll
1186 * position) and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1187 * If the element's direction is RTL, this is a negative number between `0` (initial scroll
1188 * position) and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1190 OO
.ui
.Element
.static.getScrollLeft = function ( el
) {
1191 var scrollLeft
= isRoot( el
) ? $( window
).scrollLeft() : el
.scrollLeft
;
1192 scrollLeft
= OO
.ui
.Element
.static.computeNormalizedScrollLeft( scrollLeft
, el
);
1197 * Set the number of pixels that an element's content is scrolled to the left.
1199 * See #getScrollLeft.
1203 * @param {HTMLElement|Window} el Element to scroll (and to use in calculations)
1204 * @param {number} scrollLeft Scroll position from the left.
1205 * If the element's direction is LTR, this must be a positive number between `0` (initial scroll
1206 * position) and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1207 * If the element's direction is RTL, this must be a negative number between `0` (initial scroll
1208 * position) and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1210 OO
.ui
.Element
.static.setScrollLeft = function ( el
, scrollLeft
) {
1211 scrollLeft
= OO
.ui
.Element
.static.computeNativeScrollLeft( scrollLeft
, el
);
1212 if ( isRoot( el
) ) {
1213 $( window
).scrollLeft( scrollLeft
);
1215 el
.scrollLeft
= scrollLeft
;
1221 * Get the root scrollable element of given element's document.
1223 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1224 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1225 * lets us use 'body' or 'documentElement' based on what is working.
1227 * https://code.google.com/p/chromium/issues/detail?id=303131
1230 * @param {HTMLElement} el Element to find root scrollable parent for
1231 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1232 * depending on browser
1234 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1235 var scrollTop
, body
;
1237 if ( OO
.ui
.scrollableElement
=== undefined ) {
1238 body
= el
.ownerDocument
.body
;
1239 scrollTop
= body
.scrollTop
;
1242 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1243 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1244 if ( Math
.round( body
.scrollTop
) === 1 ) {
1245 body
.scrollTop
= scrollTop
;
1246 OO
.ui
.scrollableElement
= 'body';
1248 OO
.ui
.scrollableElement
= 'documentElement';
1252 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1256 * Get closest scrollable container.
1258 * Traverses up until either a scrollable element or the root is reached, in which case the root
1259 * scrollable element will be returned (see #getRootScrollableElement).
1262 * @param {HTMLElement} el Element to find scrollable container for
1263 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1264 * @return {HTMLElement} Closest scrollable container
1266 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1268 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1269 // 'overflow-y' have different values, so we need to check the separate properties.
1270 props
= [ 'overflow-x', 'overflow-y' ],
1271 $parent
= $( el
).parent();
1273 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1274 props
= [ 'overflow-' + dimension
];
1277 // Special case for the document root (which doesn't really have any scrollable container,
1278 // since it is the ultimate scrollable container, but this is probably saner than null or
1280 if ( $( el
).is( 'html, body' ) ) {
1281 return this.getRootScrollableElement( el
);
1284 while ( $parent
.length
) {
1285 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1286 return $parent
[ 0 ];
1290 val
= $parent
.css( props
[ i
] );
1291 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will
1292 // never be scrolled in that direction, but they can actually be scrolled
1293 // programatically. The user can unintentionally perform a scroll in such case even if
1294 // the application doesn't scroll programatically, e.g. when jumping to an anchor, or
1295 // when using built-in find functionality.
1296 // This could cause funny issues...
1297 if ( val
=== 'auto' || val
=== 'scroll' ) {
1298 return $parent
[ 0 ];
1301 $parent
= $parent
.parent();
1303 // The element is unattached... return something mostly sane
1304 return this.getRootScrollableElement( el
);
1308 * Scroll element into view.
1311 * @param {HTMLElement|Object} elOrPosition Element to scroll into view
1312 * @param {Object} [config] Configuration options
1313 * @param {string} [config.animate=true] Animate to the new scroll offset.
1314 * @param {string} [config.duration='fast'] jQuery animation duration value
1315 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1316 * to scroll in both directions
1317 * @param {Object} [config.padding] Additional padding on the container to scroll past.
1318 * Object containing any of 'top', 'bottom', 'left', or 'right' as numbers.
1319 * @param {Object} [config.scrollContainer] Scroll container. Defaults to
1320 * getClosestScrollableContainer of the element.
1321 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1323 OO
.ui
.Element
.static.scrollIntoView = function ( elOrPosition
, config
) {
1324 var position
, animations
, container
, $container
, elementPosition
, containerDimensions
,
1325 $window
, padding
, animate
, method
,
1326 deferred
= $.Deferred();
1328 // Configuration initialization
1329 config
= config
|| {};
1331 padding
= $.extend( {
1336 }, config
.padding
);
1338 animate
= config
.animate
!== false;
1341 elementPosition
= elOrPosition
instanceof HTMLElement
?
1342 this.getDimensions( elOrPosition
).rect
:
1344 container
= config
.scrollContainer
|| (
1345 elOrPosition
instanceof HTMLElement
?
1346 this.getClosestScrollableContainer( elOrPosition
, config
.direction
) :
1347 // No scrollContainer or element
1348 this.getClosestScrollableContainer( document
.body
)
1350 $container
= $( container
);
1351 containerDimensions
= this.getDimensions( container
);
1352 $window
= $( this.getWindow( container
) );
1354 // Compute the element's position relative to the container
1355 if ( $container
.is( 'html, body' ) ) {
1356 // If the scrollable container is the root, this is easy
1358 top
: elementPosition
.top
,
1359 bottom
: $window
.innerHeight() - elementPosition
.bottom
,
1360 left
: elementPosition
.left
,
1361 right
: $window
.innerWidth() - elementPosition
.right
1364 // Otherwise, we have to subtract el's coordinates from container's coordinates
1366 top
: elementPosition
.top
-
1367 ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1368 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
-
1369 containerDimensions
.scrollbar
.bottom
- elementPosition
.bottom
,
1370 left
: elementPosition
.left
-
1371 ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1372 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
-
1373 containerDimensions
.scrollbar
.right
- elementPosition
.right
1377 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1378 if ( position
.top
< padding
.top
) {
1379 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
- padding
.top
;
1380 } else if ( position
.bottom
< padding
.bottom
) {
1381 animations
.scrollTop
= containerDimensions
.scroll
.top
+
1382 // Scroll the bottom into view, but not at the expense
1383 // of scrolling the top out of view
1384 Math
.min( position
.top
- padding
.top
, -position
.bottom
+ padding
.bottom
);
1387 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1388 if ( position
.left
< padding
.left
) {
1389 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
- padding
.left
;
1390 } else if ( position
.right
< padding
.right
) {
1391 animations
.scrollLeft
= containerDimensions
.scroll
.left
+
1392 // Scroll the right into view, but not at the expense
1393 // of scrolling the left out of view
1394 Math
.min( position
.left
- padding
.left
, -position
.right
+ padding
.right
);
1396 if ( animations
.scrollLeft
!== undefined ) {
1397 animations
.scrollLeft
= OO
.ui
.Element
.static.computeNativeScrollLeft( animations
.scrollLeft
, container
);
1400 if ( !$.isEmptyObject( animations
) ) {
1402 // eslint-disable-next-line no-jquery/no-animate
1403 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1404 $container
.queue( function ( next
) {
1409 $container
.stop( true );
1410 for ( method
in animations
) {
1411 $container
[ method
]( animations
[ method
] );
1418 return deferred
.promise();
1422 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1423 * and reserve space for them, because it probably doesn't.
1425 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1426 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1427 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a
1428 * reflow, and then reattach (or show) them back.
1431 * @param {HTMLElement} el Element to reconsider the scrollbars on
1433 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1434 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1435 // Save scroll position
1436 scrollLeft
= el
.scrollLeft
;
1437 scrollTop
= el
.scrollTop
;
1438 // Detach all children
1439 while ( el
.firstChild
) {
1440 nodes
.push( el
.firstChild
);
1441 el
.removeChild( el
.firstChild
);
1444 // eslint-disable-next-line no-void
1445 void el
.offsetHeight
;
1446 // Reattach all children
1447 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1448 el
.appendChild( nodes
[ i
] );
1450 // Restore scroll position (no-op if scrollbars disappeared)
1451 el
.scrollLeft
= scrollLeft
;
1452 el
.scrollTop
= scrollTop
;
1458 * Toggle visibility of an element.
1460 * @param {boolean} [show] Make element visible, omit to toggle visibility
1463 * @return {OO.ui.Element} The element, for chaining
1465 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1466 show
= show
=== undefined ? !this.visible
: !!show
;
1468 if ( show
!== this.isVisible() ) {
1469 this.visible
= show
;
1470 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1471 this.emit( 'toggle', show
);
1478 * Check if element is visible.
1480 * @return {boolean} element is visible
1482 OO
.ui
.Element
.prototype.isVisible = function () {
1483 return this.visible
;
1489 * @return {Mixed} Element data
1491 OO
.ui
.Element
.prototype.getData = function () {
1498 * @param {Mixed} data Element data
1500 * @return {OO.ui.Element} The element, for chaining
1502 OO
.ui
.Element
.prototype.setData = function ( data
) {
1508 * Set the element has an 'id' attribute.
1510 * @param {string} id
1512 * @return {OO.ui.Element} The element, for chaining
1514 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1515 this.elementId
= id
;
1516 this.$element
.attr( 'id', id
);
1521 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1522 * and return its value.
1526 OO
.ui
.Element
.prototype.getElementId = function () {
1527 if ( this.elementId
=== null ) {
1528 this.setElementId( OO
.ui
.generateElementId() );
1530 return this.elementId
;
1534 * Check if element supports one or more methods.
1536 * @param {string|string[]} methods Method or list of methods to check
1537 * @return {boolean} All methods are supported
1539 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1543 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1544 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1545 if ( typeof this[ methods
[ i
] ] === 'function' ) {
1550 return methods
.length
=== support
;
1554 * Update the theme-provided classes.
1556 * @localdoc This is called in element mixins and widget classes any time state changes.
1557 * Updating is debounced, minimizing overhead of changing multiple attributes and
1558 * guaranteeing that theme updates do not occur within an element's constructor
1560 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1561 OO
.ui
.theme
.queueUpdateElementClasses( this );
1565 * Get the HTML tag name.
1567 * Override this method to base the result on instance information.
1569 * @return {string} HTML tag name
1571 OO
.ui
.Element
.prototype.getTagName = function () {
1572 return this.constructor.static.tagName
;
1576 * Check if the element is attached to the DOM
1578 * @return {boolean} The element is attached to the DOM
1580 OO
.ui
.Element
.prototype.isElementAttached = function () {
1581 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1585 * Get the DOM document.
1587 * @return {HTMLDocument} Document object
1589 OO
.ui
.Element
.prototype.getElementDocument = function () {
1590 // Don't cache this in other ways either because subclasses could can change this.$element
1591 return OO
.ui
.Element
.static.getDocument( this.$element
);
1595 * Get the DOM window.
1597 * @return {Window} Window object
1599 OO
.ui
.Element
.prototype.getElementWindow = function () {
1600 return OO
.ui
.Element
.static.getWindow( this.$element
);
1604 * Get closest scrollable container.
1606 * @return {HTMLElement} Closest scrollable container
1608 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1609 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1613 * Get group element is in.
1615 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1617 OO
.ui
.Element
.prototype.getElementGroup = function () {
1618 return this.elementGroup
;
1622 * Set group element is in.
1624 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1626 * @return {OO.ui.Element} The element, for chaining
1628 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1629 this.elementGroup
= group
;
1634 * Scroll element into view.
1636 * @param {Object} [config] Configuration options
1637 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1639 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1641 !this.isElementAttached() ||
1642 !this.isVisible() ||
1643 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1645 return $.Deferred().resolve();
1647 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1651 * Restore the pre-infusion dynamic state for this widget.
1653 * This method is called after #$element has been inserted into DOM. The parameter is the return
1654 * value of #gatherPreInfuseState.
1657 * @param {Object} state
1659 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1663 * Wraps an HTML snippet for use with configuration values which default
1664 * to strings. This bypasses the default html-escaping done to string
1670 * @param {string} [content] HTML content
1672 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1674 this.content
= content
;
1679 OO
.initClass( OO
.ui
.HtmlSnippet
);
1686 * @return {string} Unchanged HTML snippet.
1688 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1689 return this.content
;
1693 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in
1694 * a way that is centrally controlled and can be updated dynamically. Layouts can be, and usually
1696 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout},
1697 * {@link OO.ui.FormLayout FormLayout}, {@link OO.ui.PanelLayout PanelLayout},
1698 * {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1699 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout}
1700 * for more information and examples.
1704 * @extends OO.ui.Element
1705 * @mixins OO.EventEmitter
1708 * @param {Object} [config] Configuration options
1710 OO
.ui
.Layout
= function OoUiLayout( config
) {
1711 // Configuration initialization
1712 config
= config
|| {};
1714 // Parent constructor
1715 OO
.ui
.Layout
.parent
.call( this, config
);
1717 // Mixin constructors
1718 OO
.EventEmitter
.call( this );
1721 this.$element
.addClass( 'oo-ui-layout' );
1726 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1727 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1732 * Reset scroll offsets
1735 * @return {OO.ui.Layout} The layout, for chaining
1737 OO
.ui
.Layout
.prototype.resetScroll = function () {
1738 this.$element
[ 0 ].scrollTop
= 0;
1739 OO
.ui
.Element
.static.setScrollLeft( this.$element
[ 0 ], 0 );
1745 * Widgets are compositions of one or more OOUI elements that users can both view
1746 * and interact with. All widgets can be configured and modified via a standard API,
1747 * and their state can change dynamically according to a model.
1751 * @extends OO.ui.Element
1752 * @mixins OO.EventEmitter
1755 * @param {Object} [config] Configuration options
1756 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1757 * appearance reflects this state.
1759 OO
.ui
.Widget
= function OoUiWidget( config
) {
1760 // Initialize config
1761 config
= $.extend( { disabled
: false }, config
);
1763 // Parent constructor
1764 OO
.ui
.Widget
.parent
.call( this, config
);
1766 // Mixin constructors
1767 OO
.EventEmitter
.call( this );
1770 this.disabled
= null;
1771 this.wasDisabled
= null;
1774 this.$element
.addClass( 'oo-ui-widget' );
1775 this.setDisabled( !!config
.disabled
);
1780 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1781 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1788 * A 'disable' event is emitted when the disabled state of the widget changes
1789 * (i.e. on disable **and** enable).
1791 * @param {boolean} disabled Widget is disabled
1797 * A 'toggle' event is emitted when the visibility of the widget changes.
1799 * @param {boolean} visible Widget is visible
1805 * Check if the widget is disabled.
1807 * @return {boolean} Widget is disabled
1809 OO
.ui
.Widget
.prototype.isDisabled = function () {
1810 return this.disabled
;
1814 * Set the 'disabled' state of the widget.
1816 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1818 * @param {boolean} disabled Disable widget
1820 * @return {OO.ui.Widget} The widget, for chaining
1822 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1825 this.disabled
= !!disabled
;
1826 isDisabled
= this.isDisabled();
1827 if ( isDisabled
!== this.wasDisabled
) {
1828 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1829 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1830 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1831 this.emit( 'disable', isDisabled
);
1832 this.updateThemeClasses();
1834 this.wasDisabled
= isDisabled
;
1840 * Update the disabled state, in case of changes in parent widget.
1843 * @return {OO.ui.Widget} The widget, for chaining
1845 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1846 this.setDisabled( this.disabled
);
1851 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1854 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1857 * @return {string|null} The ID of the labelable element
1859 OO
.ui
.Widget
.prototype.getInputId = function () {
1864 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1865 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1866 * override this method to provide intuitive, accessible behavior.
1868 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1869 * Individual widgets may override it too.
1871 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1874 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1885 OO
.ui
.Theme
= function OoUiTheme() {
1886 this.elementClassesQueue
= [];
1887 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1892 OO
.initClass( OO
.ui
.Theme
);
1897 * Get a list of classes to be applied to a widget.
1899 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1900 * otherwise state transitions will not work properly.
1902 * @param {OO.ui.Element} element Element for which to get classes
1903 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1905 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1906 return { on
: [], off
: [] };
1910 * Update CSS classes provided by the theme.
1912 * For elements with theme logic hooks, this should be called any time there's a state change.
1914 * @param {OO.ui.Element} element Element for which to update classes
1916 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1917 var $elements
= $( [] ),
1918 classes
= this.getElementClasses( element
);
1920 if ( element
.$icon
) {
1921 $elements
= $elements
.add( element
.$icon
);
1923 if ( element
.$indicator
) {
1924 $elements
= $elements
.add( element
.$indicator
);
1928 .removeClass( classes
.off
)
1929 .addClass( classes
.on
);
1935 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1937 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1938 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1941 this.elementClassesQueue
= [];
1945 * Queue #updateElementClasses to be called for this element.
1947 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1948 * to make them synchronous.
1950 * @param {OO.ui.Element} element Element for which to update classes
1952 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1953 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1954 // the most common case (this method is often called repeatedly for the same element).
1955 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1958 this.elementClassesQueue
.push( element
);
1959 this.debouncedUpdateQueuedElementClasses();
1963 * Get the transition duration in milliseconds for dialogs opening/closing
1965 * The dialog should be fully rendered this many milliseconds after the
1966 * ready process has executed.
1968 * @return {number} Transition duration in milliseconds
1970 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1975 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1976 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1977 * order in which users will navigate through the focusable elements via the Tab key.
1980 * // TabIndexedElement is mixed into the ButtonWidget class
1981 * // to provide a tabIndex property.
1982 * var button1 = new OO.ui.ButtonWidget( {
1986 * button2 = new OO.ui.ButtonWidget( {
1990 * button3 = new OO.ui.ButtonWidget( {
1994 * button4 = new OO.ui.ButtonWidget( {
1998 * $( document.body ).append(
2009 * @param {Object} [config] Configuration options
2010 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
2011 * the functionality is applied to the element created by the class ($element). If a different
2012 * element is specified, the tabindex functionality will be applied to it instead.
2013 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the
2014 * tab-navigation order (e.g., 1 for the first focusable element). Use 0 to use the default
2015 * navigation order; use -1 to remove the element from the tab-navigation flow.
2017 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
2018 // Configuration initialization
2019 config
= $.extend( { tabIndex
: 0 }, config
);
2022 this.$tabIndexed
= null;
2023 this.tabIndex
= null;
2026 this.connect( this, {
2027 disable
: 'onTabIndexedElementDisable'
2031 this.setTabIndex( config
.tabIndex
);
2032 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
2037 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
2042 * Set the element that should use the tabindex functionality.
2044 * This method is used to retarget a tabindex mixin so that its functionality applies
2045 * to the specified element. If an element is currently using the functionality, the mixin’s
2046 * effect on that element is removed before the new element is set up.
2048 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
2050 * @return {OO.ui.Element} The element, for chaining
2052 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
2053 var tabIndex
= this.tabIndex
;
2054 // Remove attributes from old $tabIndexed
2055 this.setTabIndex( null );
2056 // Force update of new $tabIndexed
2057 this.$tabIndexed
= $tabIndexed
;
2058 this.tabIndex
= tabIndex
;
2059 return this.updateTabIndex();
2063 * Set the value of the tabindex.
2065 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
2067 * @return {OO.ui.Element} The element, for chaining
2069 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
2070 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
2072 if ( this.tabIndex
!== tabIndex
) {
2073 this.tabIndex
= tabIndex
;
2074 this.updateTabIndex();
2081 * Update the `tabindex` attribute, in case of changes to tab index or
2086 * @return {OO.ui.Element} The element, for chaining
2088 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
2089 if ( this.$tabIndexed
) {
2090 if ( this.tabIndex
!== null ) {
2091 // Do not index over disabled elements
2092 this.$tabIndexed
.attr( {
2093 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
2094 // Support: ChromeVox and NVDA
2095 // These do not seem to inherit aria-disabled from parent elements
2096 'aria-disabled': this.isDisabled().toString()
2099 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
2106 * Handle disable events.
2109 * @param {boolean} disabled Element is disabled
2111 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
2112 this.updateTabIndex();
2116 * Get the value of the tabindex.
2118 * @return {number|null} Tabindex value
2120 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2121 return this.tabIndex
;
2125 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2127 * If the element already has an ID then that is returned, otherwise unique ID is
2128 * generated, set on the element, and returned.
2130 * @return {string|null} The ID of the focusable element
2132 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2135 if ( !this.$tabIndexed
) {
2138 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2142 id
= this.$tabIndexed
.attr( 'id' );
2143 if ( id
=== undefined ) {
2144 id
= OO
.ui
.generateElementId();
2145 this.$tabIndexed
.attr( 'id', id
);
2152 * Whether the node is 'labelable' according to the HTML spec
2153 * (i.e., whether it can be interacted with through a `<label for="…">`).
2154 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2157 * @param {jQuery} $node
2160 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2162 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2163 tagName
= ( $node
.prop( 'tagName' ) || '' ).toLowerCase();
2165 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2168 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2175 * Focus this element.
2178 * @return {OO.ui.Element} The element, for chaining
2180 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2181 if ( !this.isDisabled() ) {
2182 this.$tabIndexed
.trigger( 'focus' );
2188 * Blur this element.
2191 * @return {OO.ui.Element} The element, for chaining
2193 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2194 this.$tabIndexed
.trigger( 'blur' );
2199 * @inheritdoc OO.ui.Widget
2201 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2206 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2207 * interface element that can be configured with access keys for keyboard interaction.
2208 * See the [OOUI documentation on MediaWiki] [1] for examples.
2210 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2216 * @param {Object} [config] Configuration options
2217 * @cfg {jQuery} [$button] The button element created by the class.
2218 * If this configuration is omitted, the button element will use a generated `<a>`.
2219 * @cfg {boolean} [framed=true] Render the button with a frame
2221 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2222 // Configuration initialization
2223 config
= config
|| {};
2226 this.$button
= null;
2228 this.active
= config
.active
!== undefined && config
.active
;
2229 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
2230 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2231 this.onDocumentKeyUpHandler
= this.onDocumentKeyUp
.bind( this );
2232 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2233 this.onClickHandler
= this.onClick
.bind( this );
2234 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2237 this.$element
.addClass( 'oo-ui-buttonElement' );
2238 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2239 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2244 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2246 /* Static Properties */
2249 * Cancel mouse down events.
2251 * This property is usually set to `true` to prevent the focus from changing when the button is
2253 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and
2254 * {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} use a value of `false` so that dragging
2255 * behavior is possible and mousedown events can be handled by a parent widget.
2259 * @property {boolean}
2261 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2266 * A 'click' event is emitted when the button element is clicked.
2274 * Set the button element.
2276 * This method is used to retarget a button mixin so that its functionality applies to
2277 * the specified button element instead of the one created by the class. If a button element
2278 * is already set, the method will remove the mixin’s effect on that element.
2280 * @param {jQuery} $button Element to use as button
2282 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2283 if ( this.$button
) {
2285 .removeClass( 'oo-ui-buttonElement-button' )
2286 .removeAttr( 'role accesskey' )
2288 mousedown
: this.onMouseDownHandler
,
2289 keydown
: this.onKeyDownHandler
,
2290 click
: this.onClickHandler
,
2291 keypress
: this.onKeyPressHandler
2295 this.$button
= $button
2296 .addClass( 'oo-ui-buttonElement-button' )
2298 mousedown
: this.onMouseDownHandler
,
2299 keydown
: this.onKeyDownHandler
,
2300 click
: this.onClickHandler
,
2301 keypress
: this.onKeyPressHandler
2304 // Add `role="button"` on `<a>` elements, where it's needed
2305 // `toUpperCase()` is added for XHTML documents
2306 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2307 this.$button
.attr( 'role', 'button' );
2312 * Handles mouse down events.
2315 * @param {jQuery.Event} e Mouse down event
2316 * @return {undefined|boolean} False to prevent default if event is handled
2318 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2319 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2322 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2323 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2324 // reliably remove the pressed class
2325 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2326 // Prevent change of focus unless specifically configured otherwise
2327 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2333 * Handles document mouse up events.
2336 * @param {MouseEvent} e Mouse up event
2338 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentMouseUp = function ( e
) {
2339 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2342 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2343 // Stop listening for mouseup, since we only needed this once
2344 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2348 * Handles mouse click events.
2351 * @param {jQuery.Event} e Mouse click event
2353 * @return {undefined|boolean} False to prevent default if event is handled
2355 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2356 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2357 if ( this.emit( 'click' ) ) {
2364 * Handles key down events.
2367 * @param {jQuery.Event} e Key down event
2369 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2370 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2373 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2374 // Run the keyup handler no matter where the key is when the button is let go, so we can
2375 // reliably remove the pressed class
2376 this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2380 * Handles document key up events.
2383 * @param {KeyboardEvent} e Key up event
2385 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentKeyUp = function ( e
) {
2386 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2389 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2390 // Stop listening for keyup, since we only needed this once
2391 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2395 * Handles key press events.
2398 * @param {jQuery.Event} e Key press event
2400 * @return {undefined|boolean} False to prevent default if event is handled
2402 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2403 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2404 if ( this.emit( 'click' ) ) {
2411 * Check if button has a frame.
2413 * @return {boolean} Button is framed
2415 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2420 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame
2423 * @param {boolean} [framed] Make button framed, omit to toggle
2425 * @return {OO.ui.Element} The element, for chaining
2427 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2428 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2429 if ( framed
!== this.framed
) {
2430 this.framed
= framed
;
2432 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2433 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2434 this.updateThemeClasses();
2441 * Set the button's active state.
2443 * The active state can be set on:
2445 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2446 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2447 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2450 * @param {boolean} value Make button active
2452 * @return {OO.ui.Element} The element, for chaining
2454 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2455 this.active
= !!value
;
2456 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2457 this.updateThemeClasses();
2462 * Check if the button is active
2465 * @return {boolean} The button is active
2467 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2472 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2473 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2474 * items from the group is done through the interface the class provides.
2475 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2477 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2480 * @mixins OO.EmitterList
2484 * @param {Object} [config] Configuration options
2485 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2486 * is omitted, the group element will use a generated `<div>`.
2488 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2489 // Configuration initialization
2490 config
= config
|| {};
2492 // Mixin constructors
2493 OO
.EmitterList
.call( this, config
);
2499 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2504 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2511 * A change event is emitted when the set of selected items changes.
2513 * @param {OO.ui.Element[]} items Items currently in the group
2519 * Set the group element.
2521 * If an element is already set, items will be moved to the new element.
2523 * @param {jQuery} $group Element to use as group
2525 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2528 this.$group
= $group
;
2529 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2530 this.$group
.append( this.items
[ i
].$element
);
2535 * Find an item by its data.
2537 * Only the first item with matching data will be returned. To return all matching items,
2538 * use the #findItemsFromData method.
2540 * @param {Object} data Item data to search for
2541 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2543 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2545 hash
= OO
.getHash( data
);
2547 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2548 item
= this.items
[ i
];
2549 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2558 * Find items by their data.
2560 * All items with matching data will be returned. To return only the first match, use the
2561 * #findItemFromData method instead.
2563 * @param {Object} data Item data to search for
2564 * @return {OO.ui.Element[]} Items with equivalent data
2566 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2568 hash
= OO
.getHash( data
),
2571 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2572 item
= this.items
[ i
];
2573 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2582 * Add items to the group.
2584 * Items will be added to the end of the group array unless the optional `index` parameter
2585 * specifies a different insertion point. Adding an existing item will move it to the end of the
2586 * array or the point specified by the `index`.
2588 * @param {OO.ui.Element[]} items An array of items to add to the group
2589 * @param {number} [index] Index of the insertion point
2591 * @return {OO.ui.Element} The element, for chaining
2593 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2595 if ( items
.length
=== 0 ) {
2600 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2602 this.emit( 'change', this.getItems() );
2609 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2610 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2611 this.insertItemElements( items
, newIndex
);
2614 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2622 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2623 item
.setElementGroup( this );
2624 this.insertItemElements( item
, index
);
2627 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2633 * Insert elements into the group
2636 * @param {OO.ui.Element} itemWidget Item to insert
2637 * @param {number} index Insertion index
2639 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2640 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2641 this.$group
.append( itemWidget
.$element
);
2642 } else if ( index
=== 0 ) {
2643 this.$group
.prepend( itemWidget
.$element
);
2645 this.items
[ index
].$element
.before( itemWidget
.$element
);
2650 * Remove the specified items from a group.
2652 * Removed items are detached (not removed) from the DOM so that they may be reused.
2653 * To remove all items from a group, you may wish to use the #clearItems method instead.
2655 * @param {OO.ui.Element[]} items An array of items to remove
2657 * @return {OO.ui.Element} The element, for chaining
2659 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2660 var i
, len
, item
, index
;
2662 if ( items
.length
=== 0 ) {
2666 // Remove specific items elements
2667 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2669 index
= this.items
.indexOf( item
);
2670 if ( index
!== -1 ) {
2671 item
.setElementGroup( null );
2672 item
.$element
.detach();
2677 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2679 this.emit( 'change', this.getItems() );
2684 * Clear all items from the group.
2686 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2687 * To remove only a subset of items from a group, use the #removeItems method.
2690 * @return {OO.ui.Element} The element, for chaining
2692 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2695 // Remove all item elements
2696 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2697 this.items
[ i
].setElementGroup( null );
2698 this.items
[ i
].$element
.detach();
2702 OO
.EmitterList
.prototype.clearItems
.call( this );
2704 this.emit( 'change', this.getItems() );
2709 * LabelElement is often mixed into other classes to generate a label, which
2710 * helps identify the function of an interface element.
2711 * See the [OOUI documentation on MediaWiki] [1] for more information.
2713 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2719 * @param {Object} [config] Configuration options
2720 * @cfg {jQuery} [$label] The label element created by the class. If this
2721 * configuration is omitted, the label element will use a generated `<span>`.
2722 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be
2723 * specified as a plaintext string, a jQuery selection of elements, or a function that will
2724 * produce a string in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2725 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2726 * @cfg {boolean} [invisibleLabel] Whether the label should be visually hidden (but still
2727 * accessible to screen-readers).
2729 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2730 // Configuration initialization
2731 config
= config
|| {};
2736 this.invisibleLabel
= null;
2739 this.setLabel( config
.label
|| this.constructor.static.label
);
2740 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2741 this.setInvisibleLabel( config
.invisibleLabel
);
2746 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2751 * @event labelChange
2752 * @param {string} value
2755 /* Static Properties */
2758 * The label text. The label can be specified as a plaintext string, a function that will
2759 * produce a string in the future, or `null` for no label. The static value will
2760 * be overridden if a label is specified with the #label config option.
2764 * @property {string|Function|null}
2766 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2768 /* Static methods */
2771 * Highlight the first occurrence of the query in the given text
2773 * @param {string} text Text
2774 * @param {string} query Query to find
2775 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2776 * @return {jQuery} Text with the first match of the query
2777 * sub-string wrapped in highlighted span
2779 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2782 $result
= $( '<span>' );
2786 qLen
= query
.length
;
2787 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2788 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2793 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2796 if ( !query
.length
|| offset
=== -1 ) {
2797 $result
.text( text
);
2800 document
.createTextNode( text
.slice( 0, offset
) ),
2802 .addClass( 'oo-ui-labelElement-label-highlight' )
2803 .text( text
.slice( offset
, offset
+ query
.length
) ),
2804 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2807 return $result
.contents();
2813 * Set the label element.
2815 * If an element is already set, it will be cleaned up before setting up the new element.
2817 * @param {jQuery} $label Element to use as label
2819 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2820 if ( this.$label
) {
2821 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2824 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2825 this.setLabelContent( this.label
);
2831 * An empty string will result in the label being hidden. A string containing only whitespace will
2832 * be converted to a single ` `.
2834 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that
2835 * returns nodes or text; or null for no label
2837 * @return {OO.ui.Element} The element, for chaining
2839 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2840 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2841 label
= ( ( typeof label
=== 'string' || label
instanceof $ ) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2843 if ( this.label
!== label
) {
2844 if ( this.$label
) {
2845 this.setLabelContent( label
);
2848 this.emit( 'labelChange' );
2851 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2857 * Set whether the label should be visually hidden (but still accessible to screen-readers).
2859 * @param {boolean} invisibleLabel
2861 * @return {OO.ui.Element} The element, for chaining
2863 OO
.ui
.mixin
.LabelElement
.prototype.setInvisibleLabel = function ( invisibleLabel
) {
2864 invisibleLabel
= !!invisibleLabel
;
2866 if ( this.invisibleLabel
!== invisibleLabel
) {
2867 this.invisibleLabel
= invisibleLabel
;
2868 this.emit( 'labelChange' );
2871 this.$label
.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel
);
2872 // Pretend that there is no label, a lot of CSS has been written with this assumption
2873 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2879 * Set the label as plain text with a highlighted query
2881 * @param {string} text Text label to set
2882 * @param {string} query Substring of text to highlight
2883 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2885 * @return {OO.ui.Element} The element, for chaining
2887 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
2888 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
2894 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2895 * text; or null for no label
2897 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2902 * Set the content of the label.
2904 * Do not call this method until after the label element has been set by #setLabelElement.
2907 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2908 * text; or null for no label
2910 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2911 if ( typeof label
=== 'string' ) {
2912 if ( label
.match( /^\s*$/ ) ) {
2913 // Convert whitespace only string to a single non-breaking space
2914 this.$label
.html( ' ' );
2916 this.$label
.text( label
);
2918 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2919 this.$label
.html( label
.toString() );
2920 } else if ( label
instanceof $ ) {
2921 this.$label
.empty().append( label
);
2923 this.$label
.empty();
2928 * IconElement is often mixed into other classes to generate an icon.
2929 * Icons are graphics, about the size of normal text. They are used to aid the user
2930 * in locating a control or to convey information in a space-efficient way. See the
2931 * [OOUI documentation on MediaWiki] [1] for a list of icons
2932 * included in the library.
2934 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2940 * @param {Object} [config] Configuration options
2941 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2942 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2943 * the icon element be set to an existing icon instead of the one generated by this class, set a
2944 * value using a jQuery selection. For example:
2946 * // Use a <div> tag instead of a <span>
2947 * $icon: $( '<div>' )
2948 * // Use an existing icon element instead of the one generated by the class
2949 * $icon: this.$element
2950 * // Use an icon element from a child widget
2951 * $icon: this.childwidget.$element
2952 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a
2953 * map of symbolic names. A map is used for i18n purposes and contains a `default` icon
2954 * name and additional names keyed by language code. The `default` name is used when no icon is
2955 * keyed by the user's language.
2957 * Example of an i18n map:
2959 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2960 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2961 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2963 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2964 // Configuration initialization
2965 config
= config
|| {};
2972 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2973 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2978 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2980 /* Static Properties */
2983 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map
2984 * is used for i18n purposes and contains a `default` icon name and additional names keyed by
2985 * language code. The `default` name is used when no icon is keyed by the user's language.
2987 * Example of an i18n map:
2989 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2991 * Note: the static property will be overridden if the #icon configuration is used.
2995 * @property {Object|string}
2997 OO
.ui
.mixin
.IconElement
.static.icon
= null;
3000 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
3001 * function that returns title text, or `null` for no title.
3003 * The static property will be overridden if the #iconTitle configuration is used.
3007 * @property {string|Function|null}
3009 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
3014 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
3015 * applies to the specified icon element instead of the one created by the class. If an icon
3016 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
3017 * and mixin methods will no longer affect the element.
3019 * @param {jQuery} $icon Element to use as icon
3021 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
3024 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
3025 .removeAttr( 'title' );
3029 .addClass( 'oo-ui-iconElement-icon' )
3030 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
3031 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
3032 if ( this.iconTitle
!== null ) {
3033 this.$icon
.attr( 'title', this.iconTitle
);
3036 this.updateThemeClasses();
3040 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
3041 * The icon parameter can also be set to a map of icon names. See the #icon config setting
3044 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
3045 * by language code, or `null` to remove the icon.
3047 * @return {OO.ui.Element} The element, for chaining
3049 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
3050 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
3051 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
3053 if ( this.icon
!== icon
) {
3055 if ( this.icon
!== null ) {
3056 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
3058 if ( icon
!== null ) {
3059 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
3065 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
3067 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
3069 this.updateThemeClasses();
3075 * Get the symbolic name of the icon.
3077 * @return {string} Icon name
3079 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
3084 * IndicatorElement is often mixed into other classes to generate an indicator.
3085 * Indicators are small graphics that are generally used in two ways:
3087 * - To draw attention to the status of an item. For example, an indicator might be
3088 * used to show that an item in a list has errors that need to be resolved.
3089 * - To clarify the function of a control that acts in an exceptional way (a button
3090 * that opens a menu instead of performing an action directly, for example).
3092 * For a list of indicators included in the library, please see the
3093 * [OOUI documentation on MediaWiki] [1].
3095 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3101 * @param {Object} [config] Configuration options
3102 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
3103 * configuration is omitted, the indicator element will use a generated `<span>`.
3104 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3105 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3107 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3109 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
3110 // Configuration initialization
3111 config
= config
|| {};
3114 this.$indicator
= null;
3115 this.indicator
= null;
3118 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
3119 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
3124 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
3126 /* Static Properties */
3129 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3130 * The static property will be overridden if the #indicator configuration is used.
3134 * @property {string|null}
3136 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
3139 * A text string used as the indicator title, a function that returns title text, or `null`
3140 * for no title. The static property will be overridden if the #indicatorTitle configuration is
3145 * @property {string|Function|null}
3147 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
3152 * Set the indicator element.
3154 * If an element is already set, it will be cleaned up before setting up the new element.
3156 * @param {jQuery} $indicator Element to use as indicator
3158 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
3159 if ( this.$indicator
) {
3161 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
3162 .removeAttr( 'title' );
3165 this.$indicator
= $indicator
3166 .addClass( 'oo-ui-indicatorElement-indicator' )
3167 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
3168 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
3169 if ( this.indicatorTitle
!== null ) {
3170 this.$indicator
.attr( 'title', this.indicatorTitle
);
3173 this.updateThemeClasses();
3177 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null`
3178 * to remove the indicator.
3180 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3182 * @return {OO.ui.Element} The element, for chaining
3184 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
3185 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
3187 if ( this.indicator
!== indicator
) {
3188 if ( this.$indicator
) {
3189 if ( this.indicator
!== null ) {
3190 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
3192 if ( indicator
!== null ) {
3193 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
3196 this.indicator
= indicator
;
3199 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
3200 if ( this.$indicator
) {
3201 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
3203 this.updateThemeClasses();
3209 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3211 * @return {string} Symbolic name of indicator
3213 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
3214 return this.indicator
;
3218 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3219 * additional functionality to an element created by another class. The class provides
3220 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3221 * which are used to customize the look and feel of a widget to better describe its
3222 * importance and functionality.
3224 * The library currently contains the following styling flags for general use:
3226 * - **progressive**: Progressive styling is applied to convey that the widget will move the user
3227 * forward in a process.
3228 * - **destructive**: Destructive styling is applied to convey that the widget will remove
3231 * The flags affect the appearance of the buttons:
3234 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3235 * var button1 = new OO.ui.ButtonWidget( {
3236 * label: 'Progressive',
3237 * flags: 'progressive'
3239 * button2 = new OO.ui.ButtonWidget( {
3240 * label: 'Destructive',
3241 * flags: 'destructive'
3243 * $( document.body ).append( button1.$element, button2.$element );
3245 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an
3246 * action, use these flags: **primary** and **safe**.
3247 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3249 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3255 * @param {Object} [config] Configuration options
3256 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary')
3258 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3259 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3260 * @cfg {jQuery} [$flagged] The flagged element. By default,
3261 * the flagged functionality is applied to the element created by the class ($element).
3262 * If a different element is specified, the flagged functionality will be applied to it instead.
3264 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3265 // Configuration initialization
3266 config
= config
|| {};
3270 this.$flagged
= null;
3273 this.setFlags( config
.flags
|| this.constructor.static.flags
);
3274 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3279 OO
.initClass( OO
.ui
.mixin
.FlaggedElement
);
3285 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3286 * parameter contains the name of each modified flag and indicates whether it was
3289 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3290 * that the flag was added, `false` that the flag was removed.
3293 /* Static Properties */
3296 * Initial value to pass to setFlags if no value is provided in config.
3300 * @property {string|string[]|Object.<string, boolean>}
3302 OO
.ui
.mixin
.FlaggedElement
.static.flags
= null;
3307 * Set the flagged element.
3309 * This method is used to retarget a flagged mixin so that its functionality applies to the
3310 * specified element.
3311 * If an element is already set, the method will remove the mixin’s effect on that element.
3313 * @param {jQuery} $flagged Element that should be flagged
3315 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3316 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3317 return 'oo-ui-flaggedElement-' + flag
;
3320 if ( this.$flagged
) {
3321 this.$flagged
.removeClass( classNames
);
3324 this.$flagged
= $flagged
.addClass( classNames
);
3328 * Check if the specified flag is set.
3330 * @param {string} flag Name of flag
3331 * @return {boolean} The flag is set
3333 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3334 // This may be called before the constructor, thus before this.flags is set
3335 return this.flags
&& ( flag
in this.flags
);
3339 * Get the names of all flags set.
3341 * @return {string[]} Flag names
3343 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3344 // This may be called before the constructor, thus before this.flags is set
3345 return Object
.keys( this.flags
|| {} );
3352 * @return {OO.ui.Element} The element, for chaining
3355 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3356 var flag
, className
,
3359 classPrefix
= 'oo-ui-flaggedElement-';
3361 for ( flag
in this.flags
) {
3362 className
= classPrefix
+ flag
;
3363 changes
[ flag
] = false;
3364 delete this.flags
[ flag
];
3365 remove
.push( className
);
3368 if ( this.$flagged
) {
3369 this.$flagged
.removeClass( remove
);
3372 this.updateThemeClasses();
3373 this.emit( 'flag', changes
);
3379 * Add one or more flags.
3381 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3382 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3383 * be added (`true`) or removed (`false`).
3385 * @return {OO.ui.Element} The element, for chaining
3388 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3389 var i
, len
, flag
, className
,
3393 classPrefix
= 'oo-ui-flaggedElement-';
3395 if ( typeof flags
=== 'string' ) {
3396 className
= classPrefix
+ flags
;
3398 if ( !this.flags
[ flags
] ) {
3399 this.flags
[ flags
] = true;
3400 add
.push( className
);
3402 } else if ( Array
.isArray( flags
) ) {
3403 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3405 className
= classPrefix
+ flag
;
3407 if ( !this.flags
[ flag
] ) {
3408 changes
[ flag
] = true;
3409 this.flags
[ flag
] = true;
3410 add
.push( className
);
3413 } else if ( OO
.isPlainObject( flags
) ) {
3414 for ( flag
in flags
) {
3415 className
= classPrefix
+ flag
;
3416 if ( flags
[ flag
] ) {
3418 if ( !this.flags
[ flag
] ) {
3419 changes
[ flag
] = true;
3420 this.flags
[ flag
] = true;
3421 add
.push( className
);
3425 if ( this.flags
[ flag
] ) {
3426 changes
[ flag
] = false;
3427 delete this.flags
[ flag
];
3428 remove
.push( className
);
3434 if ( this.$flagged
) {
3437 .removeClass( remove
);
3440 this.updateThemeClasses();
3441 this.emit( 'flag', changes
);
3447 * TitledElement is mixed into other classes to provide a `title` attribute.
3448 * Titles are rendered by the browser and are made visible when the user moves
3449 * the mouse over the element. Titles are not visible on touch devices.
3452 * // TitledElement provides a `title` attribute to the
3453 * // ButtonWidget class.
3454 * var button = new OO.ui.ButtonWidget( {
3455 * label: 'Button with Title',
3456 * title: 'I am a button'
3458 * $( document.body ).append( button.$element );
3464 * @param {Object} [config] Configuration options
3465 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3466 * If this config is omitted, the title functionality is applied to $element, the
3467 * element created by the class.
3468 * @cfg {string|Function} [title] The title text or a function that returns text. If
3469 * this config is omitted, the value of the {@link #static-title static title} property is used.
3471 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3472 // Configuration initialization
3473 config
= config
|| {};
3476 this.$titled
= null;
3480 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3481 this.setTitledElement( config
.$titled
|| this.$element
);
3486 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3488 /* Static Properties */
3491 * The title text, a function that returns text, or `null` for no title. The value of the static
3492 * property is overridden if the #title config option is used.
3494 * If the element has a default title (e.g. `<input type=file>`), `null` will allow that title to be
3495 * shown. Use empty string to suppress it.
3499 * @property {string|Function|null}
3501 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3506 * Set the titled element.
3508 * This method is used to retarget a TitledElement mixin so that its functionality applies to the
3509 * specified element.
3510 * If an element is already set, the mixin’s effect on that element is removed before the new
3511 * element is set up.
3513 * @param {jQuery} $titled Element that should use the 'titled' functionality
3515 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3516 if ( this.$titled
) {
3517 this.$titled
.removeAttr( 'title' );
3520 this.$titled
= $titled
;
3527 * @param {string|Function|null} title Title text, a function that returns text, or `null`
3530 * @return {OO.ui.Element} The element, for chaining
3532 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3533 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3534 title
= typeof title
=== 'string' ? title
: null;
3536 if ( this.title
!== title
) {
3545 * Update the title attribute, in case of changes to title or accessKey.
3549 * @return {OO.ui.Element} The element, for chaining
3551 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3552 var title
= this.getTitle();
3553 if ( this.$titled
) {
3554 if ( title
!== null ) {
3555 // Only if this is an AccessKeyedElement
3556 if ( this.formatTitleWithAccessKey
) {
3557 title
= this.formatTitleWithAccessKey( title
);
3559 this.$titled
.attr( 'title', title
);
3561 this.$titled
.removeAttr( 'title' );
3570 * @return {string} Title string
3572 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3577 * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
3578 * Access keys allow an user to go to a specific element by using
3579 * a shortcut combination of a browser specific keys + the key
3583 * // AccessKeyedElement provides an `accesskey` attribute to the
3584 * // ButtonWidget class.
3585 * var button = new OO.ui.ButtonWidget( {
3586 * label: 'Button with access key',
3589 * $( document.body ).append( button.$element );
3595 * @param {Object} [config] Configuration options
3596 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3597 * If this config is omitted, the access key functionality is applied to $element, the
3598 * element created by the class.
3599 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3600 * this config is omitted, no access key will be added.
3602 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3603 // Configuration initialization
3604 config
= config
|| {};
3607 this.$accessKeyed
= null;
3608 this.accessKey
= null;
3611 this.setAccessKey( config
.accessKey
|| null );
3612 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3614 // If this is also a TitledElement and it initialized before we did, we may have
3615 // to update the title with the access key
3616 if ( this.updateTitle
) {
3623 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3625 /* Static Properties */
3628 * The access key, a function that returns a key, or `null` for no access key.
3632 * @property {string|Function|null}
3634 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3639 * Set the access keyed element.
3641 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to
3642 * the specified element.
3643 * If an element is already set, the mixin's effect on that element is removed before the new
3644 * element is set up.
3646 * @param {jQuery} $accessKeyed Element that should use the 'access keyed' functionality
3648 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3649 if ( this.$accessKeyed
) {
3650 this.$accessKeyed
.removeAttr( 'accesskey' );
3653 this.$accessKeyed
= $accessKeyed
;
3654 if ( this.accessKey
) {
3655 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3662 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no
3665 * @return {OO.ui.Element} The element, for chaining
3667 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3668 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3670 if ( this.accessKey
!== accessKey
) {
3671 if ( this.$accessKeyed
) {
3672 if ( accessKey
!== null ) {
3673 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3675 this.$accessKeyed
.removeAttr( 'accesskey' );
3678 this.accessKey
= accessKey
;
3680 // Only if this is a TitledElement
3681 if ( this.updateTitle
) {
3692 * @return {string} accessKey string
3694 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3695 return this.accessKey
;
3699 * Add information about the access key to the element's tooltip label.
3700 * (This is only public for hacky usage in FieldLayout.)
3702 * @param {string} title Tooltip label for `title` attribute
3705 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3708 if ( !this.$accessKeyed
) {
3709 // Not initialized yet; the constructor will call updateTitle() which will rerun this
3713 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the
3715 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3716 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3718 accessKey
= this.getAccessKey();
3721 title
+= ' [' + accessKey
+ ']';
3727 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3728 * feels, and functionality can be customized via the class’s configuration options
3729 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3732 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3735 * // A button widget.
3736 * var button = new OO.ui.ButtonWidget( {
3737 * label: 'Button with Icon',
3741 * $( document.body ).append( button.$element );
3743 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3746 * @extends OO.ui.Widget
3747 * @mixins OO.ui.mixin.ButtonElement
3748 * @mixins OO.ui.mixin.IconElement
3749 * @mixins OO.ui.mixin.IndicatorElement
3750 * @mixins OO.ui.mixin.LabelElement
3751 * @mixins OO.ui.mixin.TitledElement
3752 * @mixins OO.ui.mixin.FlaggedElement
3753 * @mixins OO.ui.mixin.TabIndexedElement
3754 * @mixins OO.ui.mixin.AccessKeyedElement
3757 * @param {Object} [config] Configuration options
3758 * @cfg {boolean} [active=false] Whether button should be shown as active
3759 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3760 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3761 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3763 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3764 // Configuration initialization
3765 config
= config
|| {};
3767 // Parent constructor
3768 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3770 // Mixin constructors
3771 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3772 OO
.ui
.mixin
.IconElement
.call( this, config
);
3773 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3774 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3775 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
3776 $titled
: this.$button
3778 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3779 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
3780 $tabIndexed
: this.$button
3782 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
3783 $accessKeyed
: this.$button
3789 this.noFollow
= false;
3792 this.connect( this, {
3793 disable
: 'onDisable'
3797 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3799 .addClass( 'oo-ui-buttonWidget' )
3800 .append( this.$button
);
3801 this.setActive( config
.active
);
3802 this.setHref( config
.href
);
3803 this.setTarget( config
.target
);
3804 this.setNoFollow( config
.noFollow
);
3809 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3810 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3811 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3812 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3813 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3814 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3815 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3816 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3817 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3819 /* Static Properties */
3825 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3831 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3836 * Get hyperlink location.
3838 * @return {string} Hyperlink location
3840 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3845 * Get hyperlink target.
3847 * @return {string} Hyperlink target
3849 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3854 * Get search engine traversal hint.
3856 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3858 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3859 return this.noFollow
;
3863 * Set hyperlink location.
3865 * @param {string|null} href Hyperlink location, null to remove
3867 * @return {OO.ui.Widget} The widget, for chaining
3869 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3870 href
= typeof href
=== 'string' ? href
: null;
3871 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3875 if ( href
!== this.href
) {
3884 * Update the `href` attribute, in case of changes to href or
3889 * @return {OO.ui.Widget} The widget, for chaining
3891 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3892 if ( this.href
!== null && !this.isDisabled() ) {
3893 this.$button
.attr( 'href', this.href
);
3895 this.$button
.removeAttr( 'href' );
3902 * Handle disable events.
3905 * @param {boolean} disabled Element is disabled
3907 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3912 * Set hyperlink target.
3914 * @param {string|null} target Hyperlink target, null to remove
3915 * @return {OO.ui.Widget} The widget, for chaining
3917 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3918 target
= typeof target
=== 'string' ? target
: null;
3920 if ( target
!== this.target
) {
3921 this.target
= target
;
3922 if ( target
!== null ) {
3923 this.$button
.attr( 'target', target
);
3925 this.$button
.removeAttr( 'target' );
3933 * Set search engine traversal hint.
3935 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3936 * @return {OO.ui.Widget} The widget, for chaining
3938 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3939 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3941 if ( noFollow
!== this.noFollow
) {
3942 this.noFollow
= noFollow
;
3944 this.$button
.attr( 'rel', 'nofollow' );
3946 this.$button
.removeAttr( 'rel' );
3953 // Override method visibility hints from ButtonElement
3964 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3965 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3966 * removed, and cleared from the group.
3969 * // A ButtonGroupWidget with two buttons.
3970 * var button1 = new OO.ui.PopupButtonWidget( {
3971 * label: 'Select a category',
3974 * $content: $( '<p>List of categories…</p>' ),
3979 * button2 = new OO.ui.ButtonWidget( {
3982 * buttonGroup = new OO.ui.ButtonGroupWidget( {
3983 * items: [ button1, button2 ]
3985 * $( document.body ).append( buttonGroup.$element );
3988 * @extends OO.ui.Widget
3989 * @mixins OO.ui.mixin.GroupElement
3990 * @mixins OO.ui.mixin.TitledElement
3993 * @param {Object} [config] Configuration options
3994 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3996 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3997 // Configuration initialization
3998 config
= config
|| {};
4000 // Parent constructor
4001 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
4003 // Mixin constructors
4004 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {
4005 $group
: this.$element
4007 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4010 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
4011 if ( Array
.isArray( config
.items
) ) {
4012 this.addItems( config
.items
);
4018 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
4019 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
4020 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.TitledElement
);
4022 /* Static Properties */
4028 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
4036 * @return {OO.ui.Widget} The widget, for chaining
4038 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
4039 if ( !this.isDisabled() ) {
4040 if ( this.items
[ 0 ] ) {
4041 this.items
[ 0 ].focus();
4050 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
4055 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}.
4056 * In general, IconWidgets should be used with OO.ui.LabelWidget, which creates a label that
4057 * identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
4058 * for a list of icons included in the library.
4061 * // An IconWidget with a label via LabelWidget.
4062 * var myIcon = new OO.ui.IconWidget( {
4066 * // Create a label.
4067 * iconLabel = new OO.ui.LabelWidget( {
4070 * $( document.body ).append( myIcon.$element, iconLabel.$element );
4072 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4075 * @extends OO.ui.Widget
4076 * @mixins OO.ui.mixin.IconElement
4077 * @mixins OO.ui.mixin.TitledElement
4078 * @mixins OO.ui.mixin.LabelElement
4079 * @mixins OO.ui.mixin.FlaggedElement
4082 * @param {Object} [config] Configuration options
4084 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
4085 // Configuration initialization
4086 config
= config
|| {};
4088 // Parent constructor
4089 OO
.ui
.IconWidget
.parent
.call( this, config
);
4091 // Mixin constructors
4092 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {
4093 $icon
: this.$element
4095 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4096 $titled
: this.$element
4098 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4099 $label
: this.$element
,
4100 invisibleLabel
: true
4102 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {
4103 $flagged
: this.$element
4107 this.$element
.addClass( 'oo-ui-iconWidget' );
4108 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4109 // nested in other widgets, because this widget used to not mix in LabelElement.
4110 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4115 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
4116 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
4117 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
4118 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.LabelElement
);
4119 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
4121 /* Static Properties */
4127 OO
.ui
.IconWidget
.static.tagName
= 'span';
4130 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4131 * attention to the status of an item or to clarify the function within a control. For a list of
4132 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4135 * // An indicator widget.
4136 * var indicator1 = new OO.ui.IndicatorWidget( {
4137 * indicator: 'required'
4139 * // Create a fieldset layout to add a label.
4140 * fieldset = new OO.ui.FieldsetLayout();
4141 * fieldset.addItems( [
4142 * new OO.ui.FieldLayout( indicator1, {
4143 * label: 'A required indicator:'
4146 * $( document.body ).append( fieldset.$element );
4148 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4151 * @extends OO.ui.Widget
4152 * @mixins OO.ui.mixin.IndicatorElement
4153 * @mixins OO.ui.mixin.TitledElement
4154 * @mixins OO.ui.mixin.LabelElement
4157 * @param {Object} [config] Configuration options
4159 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4160 // Configuration initialization
4161 config
= config
|| {};
4163 // Parent constructor
4164 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4166 // Mixin constructors
4167 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {
4168 $indicator
: this.$element
4170 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4171 $titled
: this.$element
4173 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4174 $label
: this.$element
,
4175 invisibleLabel
: true
4179 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4180 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4181 // nested in other widgets, because this widget used to not mix in LabelElement.
4182 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4187 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4188 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4189 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4190 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.LabelElement
);
4192 /* Static Properties */
4198 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4201 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4202 * be configured with a `label` option that is set to a string, a label node, or a function:
4204 * - String: a plaintext string
4205 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4206 * label that includes a link or special styling, such as a gray color or additional
4207 * graphical elements.
4208 * - Function: a function that will produce a string in the future. Functions are used
4209 * in cases where the value of the label is not currently defined.
4211 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget},
4212 * which will come into focus when the label is clicked.
4215 * // Two LabelWidgets.
4216 * var label1 = new OO.ui.LabelWidget( {
4217 * label: 'plaintext label'
4219 * label2 = new OO.ui.LabelWidget( {
4220 * label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4222 * // Create a fieldset layout with fields for each example.
4223 * fieldset = new OO.ui.FieldsetLayout();
4224 * fieldset.addItems( [
4225 * new OO.ui.FieldLayout( label1 ),
4226 * new OO.ui.FieldLayout( label2 )
4228 * $( document.body ).append( fieldset.$element );
4231 * @extends OO.ui.Widget
4232 * @mixins OO.ui.mixin.LabelElement
4233 * @mixins OO.ui.mixin.TitledElement
4236 * @param {Object} [config] Configuration options
4237 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4238 * Clicking the label will focus the specified input field.
4240 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4241 // Configuration initialization
4242 config
= config
|| {};
4244 // Parent constructor
4245 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4247 // Mixin constructors
4248 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4249 $label
: this.$element
4251 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4254 this.input
= config
.input
;
4258 if ( this.input
.getInputId() ) {
4259 this.$element
.attr( 'for', this.input
.getInputId() );
4261 this.$label
.on( 'click', function () {
4262 this.input
.simulateLabelClick();
4266 this.$element
.addClass( 'oo-ui-labelWidget' );
4271 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4272 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4273 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4275 /* Static Properties */
4281 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4284 * MessageWidget produces a visual component for sending a notice to the user
4285 * with an icon and distinct design noting its purpose. The MessageWidget changes
4286 * its visual presentation based on the type chosen, which also denotes its UX
4290 * @extends OO.ui.Widget
4291 * @mixins OO.ui.mixin.IconElement
4292 * @mixins OO.ui.mixin.LabelElement
4293 * @mixins OO.ui.mixin.TitledElement
4294 * @mixins OO.ui.mixin.FlaggedElement
4297 * @param {Object} [config] Configuration options
4298 * @cfg {string} [type='notice'] The type of the notice widget. This will also
4299 * impact the flags that the widget receives (and hence its CSS design) as well
4300 * as the icon that appears. Available types:
4301 * 'notice', 'error', 'warning', 'success'
4302 * @cfg {boolean} [inline] Set the notice as an inline notice. The default
4303 * is not inline, or 'boxed' style.
4305 OO
.ui
.MessageWidget
= function OoUiMessageWidget( config
) {
4306 // Configuration initialization
4307 config
= config
|| {};
4309 // Parent constructor
4310 OO
.ui
.MessageWidget
.parent
.call( this, config
);
4312 // Mixin constructors
4313 OO
.ui
.mixin
.IconElement
.call( this, config
);
4314 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4315 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4316 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
4319 this.setType( config
.type
);
4320 this.setInline( config
.inline
);
4324 .append( this.$icon
, this.$label
)
4325 .addClass( 'oo-ui-messageWidget' );
4330 OO
.inheritClass( OO
.ui
.MessageWidget
, OO
.ui
.Widget
);
4331 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.IconElement
);
4332 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.LabelElement
);
4333 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.TitledElement
);
4334 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.FlaggedElement
);
4336 /* Static Properties */
4339 * An object defining the icon name per defined type.
4342 * @property {Object}
4344 OO
.ui
.MessageWidget
.static.iconMap
= {
4345 notice
: 'infoFilled',
4354 * Set the inline state of the widget.
4356 * @param {boolean} inline Widget is inline
4358 OO
.ui
.MessageWidget
.prototype.setInline = function ( inline
) {
4361 if ( this.inline
!== inline
) {
4362 this.inline
= inline
;
4364 .toggleClass( 'oo-ui-messageWidget-block', !this.inline
);
4368 * Set the widget type. The given type must belong to the list of
4369 * legal types set by OO.ui.MessageWidget.static.iconMap
4371 * @param {string} [type] Given type. Defaults to 'notice'
4373 OO
.ui
.MessageWidget
.prototype.setType = function ( type
) {
4375 if ( Object
.keys( this.constructor.static.iconMap
).indexOf( type
) === -1 ) {
4376 type
= 'notice'; // Default
4379 if ( this.type
!== type
) {
4383 this.setFlags( type
);
4385 // Set the icon and its variant
4386 this.setIcon( this.constructor.static.iconMap
[ type
] );
4387 this.$icon
.removeClass( 'oo-ui-image-' + this.type
);
4388 this.$icon
.addClass( 'oo-ui-image-' + type
);
4390 if ( type
=== 'error' ) {
4391 this.$element
.attr( 'role', 'alert' );
4392 this.$element
.removeAttr( 'aria-live' );
4394 this.$element
.removeAttr( 'role' );
4395 this.$element
.attr( 'aria-live', 'polite' );
4403 * PendingElement is a mixin that is used to create elements that notify users that something is
4404 * happening and that they should wait before proceeding. The pending state is visually represented
4405 * with a pending texture that appears in the head of a pending
4406 * {@link OO.ui.ProcessDialog process dialog} or in the input field of a
4407 * {@link OO.ui.TextInputWidget text input widget}.
4409 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked
4410 * as pending, but only when used in {@link OO.ui.MessageDialog message dialogs}. The behavior is
4411 * not currently supported for action widgets used in process dialogs.
4414 * function MessageDialog( config ) {
4415 * MessageDialog.parent.call( this, config );
4417 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4419 * MessageDialog.static.name = 'myMessageDialog';
4420 * MessageDialog.static.actions = [
4421 * { action: 'save', label: 'Done', flags: 'primary' },
4422 * { label: 'Cancel', flags: 'safe' }
4425 * MessageDialog.prototype.initialize = function () {
4426 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4427 * this.content = new OO.ui.PanelLayout( { padded: true } );
4428 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending ' +
4429 * 'state. Note that action widgets can be marked pending in message dialogs but not ' +
4430 * 'process dialogs.</p>' );
4431 * this.$body.append( this.content.$element );
4433 * MessageDialog.prototype.getBodyHeight = function () {
4436 * MessageDialog.prototype.getActionProcess = function ( action ) {
4437 * var dialog = this;
4438 * if ( action === 'save' ) {
4439 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4440 * return new OO.ui.Process()
4442 * .next( function () {
4443 * dialog.getActions().get({actions: 'save'})[0].popPending();
4446 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4449 * var windowManager = new OO.ui.WindowManager();
4450 * $( document.body ).append( windowManager.$element );
4452 * var dialog = new MessageDialog();
4453 * windowManager.addWindows( [ dialog ] );
4454 * windowManager.openWindow( dialog );
4460 * @param {Object} [config] Configuration options
4461 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4463 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4464 // Configuration initialization
4465 config
= config
|| {};
4469 this.$pending
= null;
4472 this.setPendingElement( config
.$pending
|| this.$element
);
4477 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4482 * Set the pending element (and clean up any existing one).
4484 * @param {jQuery} $pending The element to set to pending.
4486 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4487 if ( this.$pending
) {
4488 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4491 this.$pending
= $pending
;
4492 if ( this.pending
> 0 ) {
4493 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4498 * Check if an element is pending.
4500 * @return {boolean} Element is pending
4502 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4503 return !!this.pending
;
4507 * Increase the pending counter. The pending state will remain active until the counter is zero
4508 * (i.e., the number of calls to #pushPending and #popPending is the same).
4511 * @return {OO.ui.Element} The element, for chaining
4513 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4514 if ( this.pending
=== 0 ) {
4515 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4516 this.updateThemeClasses();
4524 * Decrease the pending counter. The pending state will remain active until the counter is zero
4525 * (i.e., the number of calls to #pushPending and #popPending is the same).
4528 * @return {OO.ui.Element} The element, for chaining
4530 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4531 if ( this.pending
=== 1 ) {
4532 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4533 this.updateThemeClasses();
4535 this.pending
= Math
.max( 0, this.pending
- 1 );
4541 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4542 * in the document (for example, in an OO.ui.Window's $overlay).
4544 * The elements's position is automatically calculated and maintained when window is resized or the
4545 * page is scrolled. If you reposition the container manually, you have to call #position to make
4546 * sure the element is still placed correctly.
4548 * As positioning is only possible when both the element and the container are attached to the DOM
4549 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4550 * the #toggle method to display a floating popup, for example.
4556 * @param {Object} [config] Configuration options
4557 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4558 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4559 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4560 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4561 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4562 * 'top': Align the top edge with $floatableContainer's top edge
4563 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4564 * 'center': Vertically align the center with $floatableContainer's center
4565 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4566 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4567 * 'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
4568 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4569 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4570 * 'center': Horizontally align the center with $floatableContainer's center
4571 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4574 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4575 // Configuration initialization
4576 config
= config
|| {};
4579 this.$floatable
= null;
4580 this.$floatableContainer
= null;
4581 this.$floatableWindow
= null;
4582 this.$floatableClosestScrollable
= null;
4583 this.floatableOutOfView
= false;
4584 this.onFloatableScrollHandler
= this.position
.bind( this );
4585 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4588 this.setFloatableContainer( config
.$floatableContainer
);
4589 this.setFloatableElement( config
.$floatable
|| this.$element
);
4590 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4591 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4592 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ?
4593 true : !!config
.hideWhenOutOfView
;
4599 * Set floatable element.
4601 * If an element is already set, it will be cleaned up before setting up the new element.
4603 * @param {jQuery} $floatable Element to make floatable
4605 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4606 if ( this.$floatable
) {
4607 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4608 this.$floatable
.css( { left
: '', top
: '' } );
4611 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4616 * Set floatable container.
4618 * The element will be positioned relative to the specified container.
4620 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4622 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4623 this.$floatableContainer
= $floatableContainer
;
4624 if ( this.$floatable
) {
4630 * Change how the element is positioned vertically.
4632 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4634 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4635 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4636 throw new Error( 'Invalid value for vertical position: ' + position
);
4638 if ( this.verticalPosition
!== position
) {
4639 this.verticalPosition
= position
;
4640 if ( this.$floatable
) {
4647 * Change how the element is positioned horizontally.
4649 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4651 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4652 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4653 throw new Error( 'Invalid value for horizontal position: ' + position
);
4655 if ( this.horizontalPosition
!== position
) {
4656 this.horizontalPosition
= position
;
4657 if ( this.$floatable
) {
4664 * Toggle positioning.
4666 * Do not turn positioning on until after the element is attached to the DOM and visible.
4668 * @param {boolean} [positioning] Enable positioning, omit to toggle
4670 * @return {OO.ui.Element} The element, for chaining
4672 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4673 var closestScrollableOfContainer
;
4675 if ( !this.$floatable
|| !this.$floatableContainer
) {
4679 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4681 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4682 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4683 this.warnedUnattached
= true;
4686 if ( this.positioning
!== positioning
) {
4687 this.positioning
= positioning
;
4689 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer(
4690 this.$floatableContainer
[ 0 ]
4692 // If the scrollable is the root, we have to listen to scroll events
4693 // on the window because of browser inconsistencies.
4694 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4695 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow(
4696 closestScrollableOfContainer
4700 if ( positioning
) {
4701 this.$floatableWindow
= $( this.getElementWindow() );
4702 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4704 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4705 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4707 // Initial position after visible
4710 if ( this.$floatableWindow
) {
4711 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4712 this.$floatableWindow
= null;
4715 if ( this.$floatableClosestScrollable
) {
4716 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4717 this.$floatableClosestScrollable
= null;
4720 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4728 * Check whether the bottom edge of the given element is within the viewport of the given
4732 * @param {jQuery} $element
4733 * @param {jQuery} $container
4736 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4737 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
,
4738 rightEdgeInBounds
, startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4739 direction
= $element
.css( 'direction' );
4741 elemRect
= $element
[ 0 ].getBoundingClientRect();
4742 if ( $container
[ 0 ] === window
) {
4743 viewportSpacing
= OO
.ui
.getViewportSpacing();
4747 right
: document
.documentElement
.clientWidth
,
4748 bottom
: document
.documentElement
.clientHeight
4750 contRect
.top
+= viewportSpacing
.top
;
4751 contRect
.left
+= viewportSpacing
.left
;
4752 contRect
.right
-= viewportSpacing
.right
;
4753 contRect
.bottom
-= viewportSpacing
.bottom
;
4755 contRect
= $container
[ 0 ].getBoundingClientRect();
4758 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4759 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4760 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4761 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4762 if ( direction
=== 'rtl' ) {
4763 startEdgeInBounds
= rightEdgeInBounds
;
4764 endEdgeInBounds
= leftEdgeInBounds
;
4766 startEdgeInBounds
= leftEdgeInBounds
;
4767 endEdgeInBounds
= rightEdgeInBounds
;
4770 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4773 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4776 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4779 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4783 // The other positioning values are all about being inside the container,
4784 // so in those cases all we care about is that any part of the container is visible.
4785 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4786 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4790 * Check if the floatable is hidden to the user because it was offscreen.
4792 * @return {boolean} Floatable is out of view
4794 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4795 return this.floatableOutOfView
;
4799 * Position the floatable below its container.
4801 * This should only be done when both of them are attached to the DOM and visible.
4804 * @return {OO.ui.Element} The element, for chaining
4806 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4807 if ( !this.positioning
) {
4812 // To continue, some things need to be true:
4813 // The element must actually be in the DOM
4814 this.isElementAttached() && (
4815 // The closest scrollable is the current window
4816 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4817 // OR is an element in the element's DOM
4818 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4821 // Abort early if important parts of the widget are no longer attached to the DOM
4825 this.floatableOutOfView
= this.hideWhenOutOfView
&&
4826 !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4827 if ( this.floatableOutOfView
) {
4828 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4831 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4834 this.$floatable
.css( this.computePosition() );
4836 // We updated the position, so re-evaluate the clipping state.
4837 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4838 // will not notice the need to update itself.)
4839 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here.
4840 // Why does it not listen to the right events in the right places?
4849 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4850 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4851 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4853 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4855 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4856 var isBody
, scrollableX
, scrollableY
, containerPos
,
4857 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4858 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4859 direction
= this.$floatableContainer
.css( 'direction' ),
4860 $offsetParent
= this.$floatable
.offsetParent();
4862 if ( $offsetParent
.is( 'html' ) ) {
4863 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4864 // <html> element, but they do work on the <body>
4865 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4867 isBody
= $offsetParent
.is( 'body' );
4868 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' ||
4869 $offsetParent
.css( 'overflow-x' ) === 'auto';
4870 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' ||
4871 $offsetParent
.css( 'overflow-y' ) === 'auto';
4873 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4874 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4875 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container
4876 // is the body, or if it isn't scrollable
4877 scrollTop
= scrollableY
&& !isBody
?
4878 $offsetParent
.scrollTop() : 0;
4879 scrollLeft
= scrollableX
&& !isBody
?
4880 OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4882 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4883 // if the <body> has a margin
4884 containerPos
= isBody
?
4885 this.$floatableContainer
.offset() :
4886 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4887 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4888 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4889 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4890 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4892 if ( this.verticalPosition
=== 'below' ) {
4893 newPos
.top
= containerPos
.bottom
;
4894 } else if ( this.verticalPosition
=== 'above' ) {
4895 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4896 } else if ( this.verticalPosition
=== 'top' ) {
4897 newPos
.top
= containerPos
.top
;
4898 } else if ( this.verticalPosition
=== 'bottom' ) {
4899 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4900 } else if ( this.verticalPosition
=== 'center' ) {
4901 newPos
.top
= containerPos
.top
+
4902 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4905 if ( this.horizontalPosition
=== 'before' ) {
4906 newPos
.end
= containerPos
.start
;
4907 } else if ( this.horizontalPosition
=== 'after' ) {
4908 newPos
.start
= containerPos
.end
;
4909 } else if ( this.horizontalPosition
=== 'start' ) {
4910 newPos
.start
= containerPos
.start
;
4911 } else if ( this.horizontalPosition
=== 'end' ) {
4912 newPos
.end
= containerPos
.end
;
4913 } else if ( this.horizontalPosition
=== 'center' ) {
4914 newPos
.left
= containerPos
.left
+
4915 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4918 if ( newPos
.start
!== undefined ) {
4919 if ( direction
=== 'rtl' ) {
4920 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4921 $offsetParent
).outerWidth() - newPos
.start
;
4923 newPos
.left
= newPos
.start
;
4925 delete newPos
.start
;
4927 if ( newPos
.end
!== undefined ) {
4928 if ( direction
=== 'rtl' ) {
4929 newPos
.left
= newPos
.end
;
4931 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4932 $offsetParent
).outerWidth() - newPos
.end
;
4937 // Account for scroll position
4938 if ( newPos
.top
!== '' ) {
4939 newPos
.top
+= scrollTop
;
4941 if ( newPos
.bottom
!== '' ) {
4942 newPos
.bottom
-= scrollTop
;
4944 if ( newPos
.left
!== '' ) {
4945 newPos
.left
+= scrollLeft
;
4947 if ( newPos
.right
!== '' ) {
4948 newPos
.right
-= scrollLeft
;
4951 // Account for scrollbar gutter
4952 if ( newPos
.bottom
!== '' ) {
4953 newPos
.bottom
-= horizScrollbarHeight
;
4955 if ( direction
=== 'rtl' ) {
4956 if ( newPos
.left
!== '' ) {
4957 newPos
.left
-= vertScrollbarWidth
;
4960 if ( newPos
.right
!== '' ) {
4961 newPos
.right
-= vertScrollbarWidth
;
4969 * Element that can be automatically clipped to visible boundaries.
4971 * Whenever the element's natural height changes, you have to call
4972 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4973 * clipping correctly.
4975 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4976 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4977 * then #$clippable will be given a fixed reduced height and/or width and will be made
4978 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4979 * but you can build a static footer by setting #$clippableContainer to an element that contains
4980 * #$clippable and the footer.
4986 * @param {Object} [config] Configuration options
4987 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4988 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4989 * omit to use #$clippable
4991 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4992 // Configuration initialization
4993 config
= config
|| {};
4996 this.$clippable
= null;
4997 this.$clippableContainer
= null;
4998 this.clipping
= false;
4999 this.clippedHorizontally
= false;
5000 this.clippedVertically
= false;
5001 this.$clippableScrollableContainer
= null;
5002 this.$clippableScroller
= null;
5003 this.$clippableWindow
= null;
5004 this.idealWidth
= null;
5005 this.idealHeight
= null;
5006 this.onClippableScrollHandler
= this.clip
.bind( this );
5007 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
5010 if ( config
.$clippableContainer
) {
5011 this.setClippableContainer( config
.$clippableContainer
);
5013 this.setClippableElement( config
.$clippable
|| this.$element
);
5019 * Set clippable element.
5021 * If an element is already set, it will be cleaned up before setting up the new element.
5023 * @param {jQuery} $clippable Element to make clippable
5025 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
5026 if ( this.$clippable
) {
5027 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
5028 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
5029 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5032 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
5037 * Set clippable container.
5039 * This is the container that will be measured when deciding whether to clip. When clipping,
5040 * #$clippable will be resized in order to keep the clippable container fully visible.
5042 * If the clippable container is unset, #$clippable will be used.
5044 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
5046 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
5047 this.$clippableContainer
= $clippableContainer
;
5048 if ( this.$clippable
) {
5056 * Do not turn clipping on until after the element is attached to the DOM and visible.
5058 * @param {boolean} [clipping] Enable clipping, omit to toggle
5060 * @return {OO.ui.Element} The element, for chaining
5062 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
5063 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
5065 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5066 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
5067 this.warnedUnattached
= true;
5070 if ( this.clipping
!== clipping
) {
5071 this.clipping
= clipping
;
5073 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
5074 // If the clippable container is the root, we have to listen to scroll events and check
5075 // jQuery.scrollTop on the window because of browser inconsistencies
5076 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
5077 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
5078 this.$clippableScrollableContainer
;
5079 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
5080 this.$clippableWindow
= $( this.getElementWindow() )
5081 .on( 'resize', this.onClippableWindowResizeHandler
);
5082 // Initial clip after visible
5085 this.$clippable
.css( {
5093 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5095 this.$clippableScrollableContainer
= null;
5096 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
5097 this.$clippableScroller
= null;
5098 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
5099 this.$clippableWindow
= null;
5107 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5109 * @return {boolean} Element will be clipped to the visible area
5111 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
5112 return this.clipping
;
5116 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5118 * @return {boolean} Part of the element is being clipped
5120 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
5121 return this.clippedHorizontally
|| this.clippedVertically
;
5125 * Check if the right of the element is being clipped by the nearest scrollable container.
5127 * @return {boolean} Part of the element is being clipped
5129 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
5130 return this.clippedHorizontally
;
5134 * Check if the bottom of the element is being clipped by the nearest scrollable container.
5136 * @return {boolean} Part of the element is being clipped
5138 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
5139 return this.clippedVertically
;
5143 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
5145 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
5146 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
5148 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
5149 this.idealWidth
= width
;
5150 this.idealHeight
= height
;
5152 if ( !this.clipping
) {
5153 // Update dimensions
5154 this.$clippable
.css( { width
: width
, height
: height
} );
5156 // While clipping, idealWidth and idealHeight are not considered
5160 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5161 * ClippableElement will clip the opposite side when reducing element's width.
5163 * Classes that mix in ClippableElement should override this to return 'right' if their
5164 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
5165 * If your class also mixes in FloatableElement, this is handled automatically.
5167 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5168 * always in pixels, even if they were unset or set to 'auto'.)
5170 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
5172 * @return {string} 'left' or 'right'
5174 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
5175 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
5182 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5183 * ClippableElement will clip the opposite side when reducing element's width.
5185 * Classes that mix in ClippableElement should override this to return 'bottom' if their
5186 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
5187 * If your class also mixes in FloatableElement, this is handled automatically.
5189 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5190 * always in pixels, even if they were unset or set to 'auto'.)
5192 * When in doubt, 'top' is a sane fallback.
5194 * @return {string} 'top' or 'bottom'
5196 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
5197 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
5204 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5205 * when the element's natural height changes.
5207 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5208 * overlapped by, the visible area of the nearest scrollable container.
5210 * Because calling clip() when the natural height changes isn't always possible, we also set
5211 * max-height when the element isn't being clipped. This means that if the element tries to grow
5212 * beyond the edge, something reasonable will happen before clip() is called.
5215 * @return {OO.ui.Element} The element, for chaining
5217 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
5218 var extraHeight
, extraWidth
, viewportSpacing
,
5219 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
5220 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
5221 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
5222 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
5223 // Extra tolerance so that the sloppy code below doesn't result in results that are off
5224 // by one or two pixels. (And also so that we have space to display drop shadows.)
5225 // Chosen by fair dice roll.
5228 if ( !this.clipping
) {
5229 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below
5234 function rectIntersection( a
, b
) {
5236 out
.top
= Math
.max( a
.top
, b
.top
);
5237 out
.left
= Math
.max( a
.left
, b
.left
);
5238 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
5239 out
.right
= Math
.min( a
.right
, b
.right
);
5243 viewportSpacing
= OO
.ui
.getViewportSpacing();
5245 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
5246 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
5247 // Dimensions of the browser window, rather than the element!
5251 right
: document
.documentElement
.clientWidth
,
5252 bottom
: document
.documentElement
.clientHeight
5254 viewportRect
.top
+= viewportSpacing
.top
;
5255 viewportRect
.left
+= viewportSpacing
.left
;
5256 viewportRect
.right
-= viewportSpacing
.right
;
5257 viewportRect
.bottom
-= viewportSpacing
.bottom
;
5259 $viewport
= this.$clippableScrollableContainer
;
5260 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
5261 // Convert into a plain object
5262 viewportRect
= $.extend( {}, viewportRect
);
5265 // Account for scrollbar gutter
5266 direction
= $viewport
.css( 'direction' );
5267 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
5268 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
5269 viewportRect
.bottom
-= horizScrollbarHeight
;
5270 if ( direction
=== 'rtl' ) {
5271 viewportRect
.left
+= vertScrollbarWidth
;
5273 viewportRect
.right
-= vertScrollbarWidth
;
5276 // Add arbitrary tolerance
5277 viewportRect
.top
+= buffer
;
5278 viewportRect
.left
+= buffer
;
5279 viewportRect
.right
-= buffer
;
5280 viewportRect
.bottom
-= buffer
;
5282 $item
= this.$clippableContainer
|| this.$clippable
;
5284 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
5285 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
5287 itemRect
= $item
[ 0 ].getBoundingClientRect();
5288 // Convert into a plain object
5289 itemRect
= $.extend( {}, itemRect
);
5291 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5292 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5293 if ( this.getHorizontalAnchorEdge() === 'right' ) {
5294 itemRect
.left
= viewportRect
.left
;
5296 itemRect
.right
= viewportRect
.right
;
5298 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5299 itemRect
.top
= viewportRect
.top
;
5301 itemRect
.bottom
= viewportRect
.bottom
;
5304 availableRect
= rectIntersection( viewportRect
, itemRect
);
5306 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5307 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5308 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5309 desiredWidth
= Math
.min( desiredWidth
,
5310 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5311 desiredHeight
= Math
.min( desiredHeight
,
5312 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5313 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5314 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5315 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5316 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5317 clipWidth
= allotedWidth
< naturalWidth
;
5318 clipHeight
= allotedHeight
< naturalHeight
;
5321 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5323 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5325 this.$clippable
.css( 'overflowX', 'scroll' );
5326 // eslint-disable-next-line no-void
5327 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5328 this.$clippable
.css( {
5329 width
: Math
.max( 0, allotedWidth
),
5333 this.$clippable
.css( {
5335 width
: this.idealWidth
|| '',
5336 maxWidth
: Math
.max( 0, allotedWidth
)
5340 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5342 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5344 this.$clippable
.css( 'overflowY', 'scroll' );
5345 // eslint-disable-next-line no-void
5346 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5347 this.$clippable
.css( {
5348 height
: Math
.max( 0, allotedHeight
),
5352 this.$clippable
.css( {
5354 height
: this.idealHeight
|| '',
5355 maxHeight
: Math
.max( 0, allotedHeight
)
5359 // If we stopped clipping in at least one of the dimensions
5360 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5361 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5364 this.clippedHorizontally
= clipWidth
;
5365 this.clippedVertically
= clipHeight
;
5371 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5372 * By default, each popup has an anchor that points toward its origin.
5373 * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
5375 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5379 * var popup = new OO.ui.PopupWidget( {
5380 * $content: $( '<p>Hi there!</p>' ),
5385 * $( document.body ).append( popup.$element );
5386 * // To display the popup, toggle the visibility to 'true'.
5387 * popup.toggle( true );
5389 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5392 * @extends OO.ui.Widget
5393 * @mixins OO.ui.mixin.LabelElement
5394 * @mixins OO.ui.mixin.ClippableElement
5395 * @mixins OO.ui.mixin.FloatableElement
5398 * @param {Object} [config] Configuration options
5399 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5400 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5401 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5402 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5403 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5404 * of $floatableContainer
5405 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5406 * of $floatableContainer
5407 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5408 * endwards (right/left) to the vertical center of $floatableContainer
5409 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5410 * startwards (left/right) to the vertical center of $floatableContainer
5411 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5412 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in
5413 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5414 * move the popup as far downwards as possible.
5415 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in
5416 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5417 * move the popup as far upwards as possible.
5418 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the
5419 * center of the popup with the center of $floatableContainer.
5420 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5421 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5422 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5423 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5424 * desired direction to display the popup without clipping
5425 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5426 * See the [OOUI docs on MediaWiki][3] for an example.
5427 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5428 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a
5430 * @cfg {jQuery} [$content] Content to append to the popup's body
5431 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5432 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5433 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5434 * This config option is only relevant if #autoClose is set to `true`. See the
5435 * [OOUI documentation on MediaWiki][2] for an example.
5436 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5437 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5439 * @cfg {boolean} [padded=false] Add padding to the popup's body
5441 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5442 // Configuration initialization
5443 config
= config
|| {};
5445 // Parent constructor
5446 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5448 // Properties (must be set before ClippableElement constructor call)
5449 this.$body
= $( '<div>' );
5450 this.$popup
= $( '<div>' );
5452 // Mixin constructors
5453 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5454 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {
5455 $clippable
: this.$body
,
5456 $clippableContainer
: this.$popup
5458 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5461 this.$anchor
= $( '<div>' );
5462 // If undefined, will be computed lazily in computePosition()
5463 this.$container
= config
.$container
;
5464 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5465 this.autoClose
= !!config
.autoClose
;
5466 this.transitionTimeout
= null;
5467 this.anchored
= false;
5468 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5469 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5472 this.setSize( config
.width
, config
.height
);
5473 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5474 this.setAlignment( config
.align
|| 'center' );
5475 this.setPosition( config
.position
|| 'below' );
5476 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5477 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5478 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5479 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5481 .addClass( 'oo-ui-popupWidget-popup' )
5482 .append( this.$body
);
5484 .addClass( 'oo-ui-popupWidget' )
5485 .append( this.$popup
, this.$anchor
);
5486 // Move content, which was added to #$element by OO.ui.Widget, to the body
5487 // FIXME This is gross, we should use '$body' or something for the config
5488 if ( config
.$content
instanceof $ ) {
5489 this.$body
.append( config
.$content
);
5492 if ( config
.padded
) {
5493 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5496 if ( config
.head
) {
5497 this.closeButton
= new OO
.ui
.ButtonWidget( {
5501 this.closeButton
.connect( this, {
5502 click
: 'onCloseButtonClick'
5504 this.$head
= $( '<div>' )
5505 .addClass( 'oo-ui-popupWidget-head' )
5506 .append( this.$label
, this.closeButton
.$element
);
5507 this.$popup
.prepend( this.$head
);
5510 if ( config
.$footer
) {
5511 this.$footer
= $( '<div>' )
5512 .addClass( 'oo-ui-popupWidget-footer' )
5513 .append( config
.$footer
);
5514 this.$popup
.append( this.$footer
);
5517 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5518 // that reference properties not initialized at that time of parent class construction
5519 // TODO: Find a better way to handle post-constructor setup
5520 this.visible
= false;
5521 this.$element
.addClass( 'oo-ui-element-hidden' );
5526 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5527 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5528 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5529 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5536 * The popup is ready: it is visible and has been positioned and clipped.
5542 * Handles document mouse down events.
5545 * @param {MouseEvent} e Mouse down event
5547 OO
.ui
.PopupWidget
.prototype.onDocumentMouseDown = function ( e
) {
5550 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5552 this.toggle( false );
5557 * Bind document mouse down listener.
5561 OO
.ui
.PopupWidget
.prototype.bindDocumentMouseDownListener = function () {
5562 // Capture clicks outside popup
5563 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5564 // We add 'click' event because iOS safari needs to respond to this event.
5565 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5566 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5567 // of occasionally not emitting 'click' properly, that event seems to be the standard
5568 // that it should be emitting, so we add it to this and will operate the event handler
5569 // on whichever of these events was triggered first
5570 this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5574 * Handles close button click events.
5578 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5579 if ( this.isVisible() ) {
5580 this.toggle( false );
5585 * Unbind document mouse down listener.
5589 OO
.ui
.PopupWidget
.prototype.unbindDocumentMouseDownListener = function () {
5590 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5591 this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5595 * Handles document key down events.
5598 * @param {KeyboardEvent} e Key down event
5600 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5602 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5605 this.toggle( false );
5607 e
.stopPropagation();
5612 * Bind document key down listener.
5616 OO
.ui
.PopupWidget
.prototype.bindDocumentKeyDownListener = function () {
5617 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5621 * Unbind document key down listener.
5625 OO
.ui
.PopupWidget
.prototype.unbindDocumentKeyDownListener = function () {
5626 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5630 * Show, hide, or toggle the visibility of the anchor.
5632 * @param {boolean} [show] Show anchor, omit to toggle
5634 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5635 show
= show
=== undefined ? !this.anchored
: !!show
;
5637 if ( this.anchored
!== show
) {
5639 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5640 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5642 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5643 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5645 this.anchored
= show
;
5650 * Change which edge the anchor appears on.
5652 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5654 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5655 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5656 throw new Error( 'Invalid value for edge: ' + edge
);
5658 if ( this.anchorEdge
!== null ) {
5659 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5661 this.anchorEdge
= edge
;
5662 if ( this.anchored
) {
5663 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5668 * Check if the anchor is visible.
5670 * @return {boolean} Anchor is visible
5672 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5673 return this.anchored
;
5677 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5678 * `.toggle( true )` after its #$element is attached to the DOM.
5680 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5681 * it in the right place and with the right dimensions only work correctly while it is attached.
5682 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5683 * strictly enforced, so currently it only generates a warning in the browser console.
5688 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5689 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5690 show
= show
=== undefined ? !this.isVisible() : !!show
;
5692 change
= show
!== this.isVisible();
5694 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5695 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5696 this.warnedUnattached
= true;
5698 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5699 // Fall back to the parent node if the floatableContainer is not set
5700 this.setFloatableContainer( this.$element
.parent() );
5703 if ( change
&& show
&& this.autoFlip
) {
5704 // Reset auto-flipping before showing the popup again. It's possible we no longer need to
5705 // flip (e.g. if the user scrolled).
5706 this.isAutoFlipped
= false;
5710 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5713 this.togglePositioning( show
&& !!this.$floatableContainer
);
5716 if ( this.autoClose
) {
5717 this.bindDocumentMouseDownListener();
5718 this.bindDocumentKeyDownListener();
5720 this.updateDimensions();
5721 this.toggleClipping( true );
5723 if ( this.autoFlip
) {
5724 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5725 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5726 // If opening the popup in the normal direction causes it to be clipped,
5727 // open in the opposite one instead
5728 normalHeight
= this.$element
.height();
5729 this.isAutoFlipped
= !this.isAutoFlipped
;
5731 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5732 // If that also causes it to be clipped, open in whichever direction
5733 // we have more space
5734 oppositeHeight
= this.$element
.height();
5735 if ( oppositeHeight
< normalHeight
) {
5736 this.isAutoFlipped
= !this.isAutoFlipped
;
5742 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5743 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5744 // If opening the popup in the normal direction causes it to be clipped,
5745 // open in the opposite one instead
5746 normalWidth
= this.$element
.width();
5747 this.isAutoFlipped
= !this.isAutoFlipped
;
5748 // Due to T180173 horizontally clipped PopupWidgets have messed up
5749 // dimensions, which causes positioning to be off. Toggle clipping back and
5750 // forth to work around.
5751 this.toggleClipping( false );
5753 this.toggleClipping( true );
5754 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5755 // If that also causes it to be clipped, open in whichever direction
5756 // we have more space
5757 oppositeWidth
= this.$element
.width();
5758 if ( oppositeWidth
< normalWidth
) {
5759 this.isAutoFlipped
= !this.isAutoFlipped
;
5760 // Due to T180173, horizontally clipped PopupWidgets have messed up
5761 // dimensions, which causes positioning to be off. Toggle clipping
5762 // back and forth to work around.
5763 this.toggleClipping( false );
5765 this.toggleClipping( true );
5772 this.emit( 'ready' );
5774 this.toggleClipping( false );
5775 if ( this.autoClose
) {
5776 this.unbindDocumentMouseDownListener();
5777 this.unbindDocumentKeyDownListener();
5786 * Set the size of the popup.
5788 * Changing the size may also change the popup's position depending on the alignment.
5790 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5791 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5792 * @param {boolean} [transition=false] Use a smooth transition
5795 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5796 this.width
= width
!== undefined ? width
: 320;
5797 this.height
= height
!== undefined ? height
: null;
5798 if ( this.isVisible() ) {
5799 this.updateDimensions( transition
);
5804 * Update the size and position.
5806 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5807 * be called automatically.
5809 * @param {boolean} [transition=false] Use a smooth transition
5812 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5815 // Prevent transition from being interrupted
5816 clearTimeout( this.transitionTimeout
);
5818 // Enable transition
5819 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5825 // Prevent transitioning after transition is complete
5826 this.transitionTimeout
= setTimeout( function () {
5827 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5830 // Prevent transitioning immediately
5831 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5838 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5839 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
,
5840 anchorPos
, anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
,
5841 floatablePos
, offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5843 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5844 popupPositionOppositeMap
= {
5852 'force-left': 'backwards',
5853 'force-right': 'forwards'
5856 'force-left': 'forwards',
5857 'force-right': 'backwards'
5869 backwards
: this.anchored
? 'before' : 'end'
5877 if ( !this.$container
) {
5878 // Lazy-initialize $container if not specified in constructor
5879 this.$container
= $( this.getClosestScrollableElementContainer() );
5881 direction
= this.$container
.css( 'direction' );
5883 // Set height and width before we do anything else, since it might cause our measurements
5884 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5886 width
: this.width
!== null ? this.width
: 'auto',
5887 height
: this.height
!== null ? this.height
: 'auto'
5890 align
= alignMap
[ direction
][ this.align
] || this.align
;
5891 popupPosition
= this.popupPosition
;
5892 if ( this.isAutoFlipped
) {
5893 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5896 // If the popup is positioned before or after, then the anchor positioning is vertical,
5897 // otherwise horizontal
5898 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5899 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5900 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5901 near
= vertical
? 'top' : 'left';
5902 far
= vertical
? 'bottom' : 'right';
5903 sizeProp
= vertical
? 'Height' : 'Width';
5904 popupSize
= vertical
?
5905 ( this.height
|| this.$popup
.height() ) :
5906 ( this.width
|| this.$popup
.width() );
5908 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5909 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5910 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5913 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5914 // Find out which property FloatableElement used for positioning, and adjust that value
5915 positionProp
= vertical
?
5916 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5917 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5919 // Figure out where the near and far edges of the popup and $floatableContainer are
5920 floatablePos
= this.$floatableContainer
.offset();
5921 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5922 // Measure where the offsetParent is and compute our position based on that and parentPosition
5923 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5924 { top
: 0, left
: 0 } :
5925 this.$element
.offsetParent().offset();
5927 if ( positionProp
=== near
) {
5928 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5929 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5931 popupPos
[ far
] = offsetParentPos
[ near
] +
5932 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5933 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5936 if ( this.anchored
) {
5937 // Position the anchor (which is positioned relative to the popup) to point to
5938 // $floatableContainer
5939 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5940 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5942 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more
5943 // space this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use
5944 // scrollWidth/Height
5945 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5946 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5947 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5948 // Not enough space for the anchor on the start side; pull the popup startwards
5949 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5950 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5951 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5952 // Not enough space for the anchor on the end side; pull the popup endwards
5953 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5954 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5956 positionAdjustment
= 0;
5959 positionAdjustment
= 0;
5962 // Check if the popup will go beyond the edge of this.$container
5963 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5964 { top
: 0, left
: 0 } :
5965 this.$container
.offset();
5966 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5967 if ( this.$container
[ 0 ] === document
.documentElement
) {
5968 viewportSpacing
= OO
.ui
.getViewportSpacing();
5969 containerPos
[ near
] += viewportSpacing
[ near
];
5970 containerPos
[ far
] -= viewportSpacing
[ far
];
5972 // Take into account how much the popup will move because of the adjustments we're going to make
5973 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5974 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5975 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5976 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5977 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5978 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5979 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5980 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5981 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5982 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5985 if ( this.anchored
) {
5986 // Adjust anchorOffset for positionAdjustment
5987 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5989 // Position the anchor
5990 anchorCss
[ start
] = anchorOffset
;
5991 this.$anchor
.css( anchorCss
);
5994 // Move the popup if needed
5995 parentPosition
[ positionProp
] += positionAdjustment
;
5997 return parentPosition
;
6001 * Set popup alignment
6003 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
6004 * `backwards` or `forwards`.
6006 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
6007 // Validate alignment
6008 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
6011 this.align
= 'center';
6017 * Get popup alignment
6019 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
6020 * `backwards` or `forwards`.
6022 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
6027 * Change the positioning of the popup.
6029 * @param {string} position 'above', 'below', 'before' or 'after'
6031 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
6032 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
6035 this.popupPosition
= position
;
6040 * Get popup positioning.
6042 * @return {string} 'above', 'below', 'before' or 'after'
6044 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
6045 return this.popupPosition
;
6049 * Set popup auto-flipping.
6051 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
6052 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
6053 * desired direction to display the popup without clipping
6055 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
6056 autoFlip
= !!autoFlip
;
6058 if ( this.autoFlip
!== autoFlip
) {
6059 this.autoFlip
= autoFlip
;
6064 * Set which elements will not close the popup when clicked.
6066 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
6068 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
6070 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
6071 this.$autoCloseIgnore
= $autoCloseIgnore
;
6075 * Get an ID of the body element, this can be used as the
6076 * `aria-describedby` attribute for an input field.
6078 * @return {string} The ID of the body element
6080 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
6081 var id
= this.$body
.attr( 'id' );
6082 if ( id
=== undefined ) {
6083 id
= OO
.ui
.generateElementId();
6084 this.$body
.attr( 'id', id
);
6090 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6091 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6092 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6093 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6099 * @param {Object} [config] Configuration options
6100 * @cfg {Object} [popup] Configuration to pass to popup
6101 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6103 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
6104 // Configuration initialization
6105 config
= config
|| {};
6108 this.popup
= new OO
.ui
.PopupWidget( $.extend(
6111 $floatableContainer
: this.$element
6115 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
6125 * @return {OO.ui.PopupWidget} Popup widget
6127 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
6132 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
6133 * which is used to display additional information or options.
6136 * // A PopupButtonWidget.
6137 * var popupButton = new OO.ui.PopupButtonWidget( {
6138 * label: 'Popup button with options',
6141 * $content: $( '<p>Additional options here.</p>' ),
6143 * align: 'force-left'
6146 * // Append the button to the DOM.
6147 * $( document.body ).append( popupButton.$element );
6150 * @extends OO.ui.ButtonWidget
6151 * @mixins OO.ui.mixin.PopupElement
6154 * @param {Object} [config] Configuration options
6155 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful
6156 * in cases where the expanded popup is larger than its containing `<div>`. The specified overlay
6157 * layer is usually on top of the containing `<div>` and has a larger area. By default, the popup
6158 * uses relative positioning.
6159 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
6161 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
6162 // Configuration initialization
6163 config
= config
|| {};
6165 // Parent constructor
6166 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
6168 // Mixin constructors
6169 OO
.ui
.mixin
.PopupElement
.call( this, config
);
6172 this.$overlay
= ( config
.$overlay
=== true ?
6173 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
6176 this.connect( this, {
6181 this.$element
.addClass( 'oo-ui-popupButtonWidget' );
6183 .addClass( 'oo-ui-popupButtonWidget-popup' )
6184 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6185 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6186 this.$overlay
.append( this.popup
.$element
);
6191 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
6192 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
6197 * Handle the button action being triggered.
6201 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
6202 this.popup
.toggle();
6206 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6208 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6213 * @mixins OO.ui.mixin.GroupElement
6216 * @param {Object} [config] Configuration options
6218 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
6219 // Mixin constructors
6220 OO
.ui
.mixin
.GroupElement
.call( this, config
);
6225 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
6230 * Set the disabled state of the widget.
6232 * This will also update the disabled state of child widgets.
6234 * @param {boolean} disabled Disable widget
6236 * @return {OO.ui.Widget} The widget, for chaining
6238 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
6242 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6243 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
6245 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6247 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6248 this.items
[ i
].updateDisabled();
6256 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6258 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group.
6259 * This allows bidirectional communication.
6261 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6269 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
6276 * Check if widget is disabled.
6278 * Checks parent if present, making disabled state inheritable.
6280 * @return {boolean} Widget is disabled
6282 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
6283 return this.disabled
||
6284 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
6288 * Set group element is in.
6290 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6292 * @return {OO.ui.Widget} The widget, for chaining
6294 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
6296 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6297 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
6299 // Initialize item disabled states
6300 this.updateDisabled();
6306 * OptionWidgets are special elements that can be selected and configured with data. The
6307 * data is often unique for each option, but it does not have to be. OptionWidgets are used
6308 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6309 * and examples, please see the [OOUI documentation on MediaWiki][1].
6311 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6314 * @extends OO.ui.Widget
6315 * @mixins OO.ui.mixin.ItemWidget
6316 * @mixins OO.ui.mixin.LabelElement
6317 * @mixins OO.ui.mixin.FlaggedElement
6318 * @mixins OO.ui.mixin.AccessKeyedElement
6319 * @mixins OO.ui.mixin.TitledElement
6322 * @param {Object} [config] Configuration options
6324 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
6325 // Configuration initialization
6326 config
= config
|| {};
6328 // Parent constructor
6329 OO
.ui
.OptionWidget
.parent
.call( this, config
);
6331 // Mixin constructors
6332 OO
.ui
.mixin
.ItemWidget
.call( this );
6333 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6334 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6335 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6336 OO
.ui
.mixin
.TitledElement
.call( this, config
);
6339 this.highlighted
= false;
6340 this.pressed
= false;
6341 this.setSelected( !!config
.selected
);
6345 .data( 'oo-ui-optionWidget', this )
6346 // Allow programmatic focussing (and by access key), but not tabbing
6347 .attr( 'tabindex', '-1' )
6348 .attr( 'role', 'option' )
6349 .addClass( 'oo-ui-optionWidget' )
6350 .append( this.$label
);
6355 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6356 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6357 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6358 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6359 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6360 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.TitledElement
);
6362 /* Static Properties */
6365 * Whether this option can be selected. See #setSelected.
6369 * @property {boolean}
6371 OO
.ui
.OptionWidget
.static.selectable
= true;
6374 * Whether this option can be highlighted. See #setHighlighted.
6378 * @property {boolean}
6380 OO
.ui
.OptionWidget
.static.highlightable
= true;
6383 * Whether this option can be pressed. See #setPressed.
6387 * @property {boolean}
6389 OO
.ui
.OptionWidget
.static.pressable
= true;
6392 * Whether this option will be scrolled into view when it is selected.
6396 * @property {boolean}
6398 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6403 * Check if the option can be selected.
6405 * @return {boolean} Item is selectable
6407 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6408 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6412 * Check if the option can be highlighted. A highlight indicates that the option
6413 * may be selected when a user presses Enter key or clicks. Disabled items cannot
6416 * @return {boolean} Item is highlightable
6418 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6419 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6423 * Check if the option can be pressed. The pressed state occurs when a user mouses
6424 * down on an item, but has not yet let go of the mouse.
6426 * @return {boolean} Item is pressable
6428 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6429 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6433 * Check if the option is selected.
6435 * @return {boolean} Item is selected
6437 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6438 return this.selected
;
6442 * Check if the option is highlighted. A highlight indicates that the
6443 * item may be selected when a user presses Enter key or clicks.
6445 * @return {boolean} Item is highlighted
6447 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6448 return this.highlighted
;
6452 * Check if the option is pressed. The pressed state occurs when a user mouses
6453 * down on an item, but has not yet let go of the mouse. The item may appear
6454 * selected, but it will not be selected until the user releases the mouse.
6456 * @return {boolean} Item is pressed
6458 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6459 return this.pressed
;
6463 * Set the option’s selected state. In general, all modifications to the selection
6464 * should be handled by the SelectWidget’s
6465 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
6467 * @param {boolean} [state=false] Select option
6469 * @return {OO.ui.Widget} The widget, for chaining
6471 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6472 if ( this.constructor.static.selectable
) {
6473 this.selected
= !!state
;
6475 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6476 .attr( 'aria-selected', state
.toString() );
6477 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6478 this.scrollElementIntoView();
6480 this.updateThemeClasses();
6486 * Set the option’s highlighted state. In general, all programmatic
6487 * modifications to the highlight should be handled by the
6488 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6489 * method instead of this method.
6491 * @param {boolean} [state=false] Highlight option
6493 * @return {OO.ui.Widget} The widget, for chaining
6495 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6496 if ( this.constructor.static.highlightable
) {
6497 this.highlighted
= !!state
;
6498 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6499 this.updateThemeClasses();
6505 * Set the option’s pressed state. In general, all
6506 * programmatic modifications to the pressed state should be handled by the
6507 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6508 * method instead of this method.
6510 * @param {boolean} [state=false] Press option
6512 * @return {OO.ui.Widget} The widget, for chaining
6514 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6515 if ( this.constructor.static.pressable
) {
6516 this.pressed
= !!state
;
6517 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6518 this.updateThemeClasses();
6524 * Get text to match search strings against.
6526 * The default implementation returns the label text, but subclasses
6527 * can override this to provide more complex behavior.
6529 * @return {string|boolean} String to match search string against
6531 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6532 var label
= this.getLabel();
6533 return typeof label
=== 'string' ? label
: this.$label
.text();
6537 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6538 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6539 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6542 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For
6543 * more information, please see the [OOUI documentation on MediaWiki][1].
6546 * // A select widget with three options.
6547 * var select = new OO.ui.SelectWidget( {
6549 * new OO.ui.OptionWidget( {
6551 * label: 'Option One',
6553 * new OO.ui.OptionWidget( {
6555 * label: 'Option Two',
6557 * new OO.ui.OptionWidget( {
6559 * label: 'Option Three',
6563 * $( document.body ).append( select.$element );
6565 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6569 * @extends OO.ui.Widget
6570 * @mixins OO.ui.mixin.GroupWidget
6573 * @param {Object} [config] Configuration options
6574 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6575 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6576 * the [OOUI documentation on MediaWiki] [2] for examples.
6577 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6578 * @cfg {boolean} [multiselect] Allow for multiple selections
6580 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6581 // Configuration initialization
6582 config
= config
|| {};
6584 // Parent constructor
6585 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6587 // Mixin constructors
6588 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {
6589 $group
: this.$element
6593 this.pressed
= false;
6594 this.selecting
= null;
6595 this.multiselect
= !!config
.multiselect
;
6596 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
6597 this.onDocumentMouseMoveHandler
= this.onDocumentMouseMove
.bind( this );
6598 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
6599 this.onDocumentKeyPressHandler
= this.onDocumentKeyPress
.bind( this );
6600 this.keyPressBuffer
= '';
6601 this.keyPressBufferTimer
= null;
6602 this.blockMouseOverEvents
= 0;
6605 this.connect( this, {
6609 focusin
: this.onFocus
.bind( this ),
6610 mousedown
: this.onMouseDown
.bind( this ),
6611 mouseover
: this.onMouseOver
.bind( this ),
6612 mouseleave
: this.onMouseLeave
.bind( this )
6617 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-unpressed' )
6618 .attr( 'role', 'listbox' );
6619 this.setFocusOwner( this.$element
);
6620 if ( Array
.isArray( config
.items
) ) {
6621 this.addItems( config
.items
);
6627 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6628 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6635 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6637 * @param {OO.ui.OptionWidget|null} item Highlighted item
6643 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6644 * pressed state of an option.
6646 * @param {OO.ui.OptionWidget|null} item Pressed item
6652 * A `select` event is emitted when the selection is modified programmatically with the #selectItem
6655 * @param {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} items Currently selected items
6661 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6663 * @param {OO.ui.OptionWidget} item Chosen item
6664 * @param {boolean} selected Item is selected
6670 * An `add` event is emitted when options are added to the select with the #addItems method.
6672 * @param {OO.ui.OptionWidget[]} items Added items
6673 * @param {number} index Index of insertion point
6679 * A `remove` event is emitted when options are removed from the select with the #clearItems
6680 * or #removeItems methods.
6682 * @param {OO.ui.OptionWidget[]} items Removed items
6685 /* Static methods */
6688 * Normalize text for filter matching
6690 * @param {string} text Text
6691 * @return {string} Normalized text
6693 OO
.ui
.SelectWidget
.static.normalizeForMatching = function ( text
) {
6694 // Replace trailing whitespace, normalize multiple spaces and make case insensitive
6695 var normalized
= text
.trim().replace( /\s+/, ' ' ).toLowerCase();
6697 // Normalize Unicode
6698 // eslint-disable-next-line no-restricted-properties
6699 if ( normalized
.normalize
) {
6700 // eslint-disable-next-line no-restricted-properties
6701 normalized
= normalized
.normalize();
6709 * Handle focus events
6712 * @param {jQuery.Event} event
6714 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6716 if ( event
.target
=== this.$element
[ 0 ] ) {
6717 // This widget was focussed, e.g. by the user tabbing to it.
6718 // The styles for focus state depend on one of the items being selected.
6719 if ( !this.findSelectedItem() ) {
6720 item
= this.findFirstSelectableItem();
6723 if ( event
.target
.tabIndex
=== -1 ) {
6724 // One of the options got focussed (and the event bubbled up here).
6725 // They can't be tabbed to, but they can be activated using access keys.
6726 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6727 item
= this.findTargetItem( event
);
6729 // There is something actually user-focusable in one of the labels of the options, and
6730 // the user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change
6737 if ( item
.constructor.static.highlightable
) {
6738 this.highlightItem( item
);
6740 this.selectItem( item
);
6744 if ( event
.target
!== this.$element
[ 0 ] ) {
6745 this.$focusOwner
.trigger( 'focus' );
6750 * Handle mouse down events.
6753 * @param {jQuery.Event} e Mouse down event
6754 * @return {undefined|boolean} False to prevent default if event is handled
6756 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6759 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6760 this.togglePressed( true );
6761 item
= this.findTargetItem( e
);
6762 if ( item
&& item
.isSelectable() ) {
6763 this.pressItem( item
);
6764 this.selecting
= item
;
6765 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6766 this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6773 * Handle document mouse up events.
6776 * @param {MouseEvent} e Mouse up event
6777 * @return {undefined|boolean} False to prevent default if event is handled
6779 OO
.ui
.SelectWidget
.prototype.onDocumentMouseUp = function ( e
) {
6782 this.togglePressed( false );
6783 if ( !this.selecting
) {
6784 item
= this.findTargetItem( e
);
6785 if ( item
&& item
.isSelectable() ) {
6786 this.selecting
= item
;
6789 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6790 this.pressItem( null );
6791 this.chooseItem( this.selecting
);
6792 this.selecting
= null;
6795 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6796 this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6802 * Handle document mouse move events.
6805 * @param {MouseEvent} e Mouse move event
6807 OO
.ui
.SelectWidget
.prototype.onDocumentMouseMove = function ( e
) {
6810 if ( !this.isDisabled() && this.pressed
) {
6811 item
= this.findTargetItem( e
);
6812 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6813 this.pressItem( item
);
6814 this.selecting
= item
;
6820 * Handle mouse over events.
6823 * @param {jQuery.Event} e Mouse over event
6824 * @return {undefined|boolean} False to prevent default if event is handled
6826 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6828 if ( this.blockMouseOverEvents
) {
6831 if ( !this.isDisabled() ) {
6832 item
= this.findTargetItem( e
);
6833 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6839 * Handle mouse leave events.
6842 * @param {jQuery.Event} e Mouse over event
6843 * @return {undefined|boolean} False to prevent default if event is handled
6845 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6846 if ( !this.isDisabled() ) {
6847 this.highlightItem( null );
6853 * Handle document key down events.
6856 * @param {KeyboardEvent} e Key down event
6858 OO
.ui
.SelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
6861 selected
= this.findSelectedItems(),
6862 currentItem
= this.findHighlightedItem() || (
6863 Array
.isArray( selected
) ? selected
[ 0 ] : selected
6865 firstItem
= this.getItems()[ 0 ];
6867 if ( !this.isDisabled() && this.isVisible() ) {
6868 switch ( e
.keyCode
) {
6869 case OO
.ui
.Keys
.ENTER
:
6870 if ( currentItem
) {
6871 // Was only highlighted, now let's select it. No-op if already selected.
6872 this.chooseItem( currentItem
);
6877 case OO
.ui
.Keys
.LEFT
:
6878 this.clearKeyPressBuffer();
6879 nextItem
= currentItem
?
6880 this.findRelativeSelectableItem( currentItem
, -1 ) : firstItem
;
6883 case OO
.ui
.Keys
.DOWN
:
6884 case OO
.ui
.Keys
.RIGHT
:
6885 this.clearKeyPressBuffer();
6886 nextItem
= currentItem
?
6887 this.findRelativeSelectableItem( currentItem
, 1 ) : firstItem
;
6890 case OO
.ui
.Keys
.ESCAPE
:
6891 case OO
.ui
.Keys
.TAB
:
6892 if ( currentItem
) {
6893 currentItem
.setHighlighted( false );
6895 this.unbindDocumentKeyDownListener();
6896 this.unbindDocumentKeyPressListener();
6897 // Don't prevent tabbing away / defocusing
6903 if ( nextItem
.constructor.static.highlightable
) {
6904 this.highlightItem( nextItem
);
6906 this.chooseItem( nextItem
);
6908 this.scrollItemIntoView( nextItem
);
6913 e
.stopPropagation();
6919 * Bind document key down listener.
6923 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyDownListener = function () {
6924 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6928 * Unbind document key down listener.
6932 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
6933 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6937 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6939 * @param {OO.ui.OptionWidget} item Item to scroll into view
6941 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6943 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic
6944 // scrolling and around 100-150 ms after it is finished.
6945 this.blockMouseOverEvents
++;
6946 item
.scrollElementIntoView().done( function () {
6947 setTimeout( function () {
6948 widget
.blockMouseOverEvents
--;
6954 * Clear the key-press buffer
6958 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6959 if ( this.keyPressBufferTimer
) {
6960 clearTimeout( this.keyPressBufferTimer
);
6961 this.keyPressBufferTimer
= null;
6963 this.keyPressBuffer
= '';
6967 * Handle key press events.
6970 * @param {KeyboardEvent} e Key press event
6971 * @return {undefined|boolean} False to prevent default if event is handled
6973 OO
.ui
.SelectWidget
.prototype.onDocumentKeyPress = function ( e
) {
6974 var c
, filter
, item
, selected
;
6976 if ( !e
.charCode
) {
6977 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6978 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6983 // eslint-disable-next-line no-restricted-properties
6984 if ( String
.fromCodePoint
) {
6985 // eslint-disable-next-line no-restricted-properties
6986 c
= String
.fromCodePoint( e
.charCode
);
6988 c
= String
.fromCharCode( e
.charCode
);
6991 if ( this.keyPressBufferTimer
) {
6992 clearTimeout( this.keyPressBufferTimer
);
6994 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6996 selected
= this.findSelectedItems();
6997 item
= this.findHighlightedItem() || (
6998 Array
.isArray( selected
) ? selected
[ 0 ] : selected
7001 if ( this.keyPressBuffer
=== c
) {
7002 // Common (if weird) special case: typing "xxxx" will cycle through all
7003 // the items beginning with "x".
7005 item
= this.findRelativeSelectableItem( item
, 1 );
7008 this.keyPressBuffer
+= c
;
7011 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
7012 if ( !item
|| !filter( item
) ) {
7013 item
= this.findRelativeSelectableItem( item
, 1, filter
);
7016 if ( this.isVisible() && item
.constructor.static.highlightable
) {
7017 this.highlightItem( item
);
7019 this.chooseItem( item
);
7021 this.scrollItemIntoView( item
);
7025 e
.stopPropagation();
7029 * Get a matcher for the specific string
7032 * @param {string} query String to match against items
7033 * @param {string} [mode='prefix'] Matching mode: 'substring', 'prefix', or 'exact'
7034 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
7036 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( query
, mode
) {
7037 var normalizeForMatching
= this.constructor.static.normalizeForMatching
,
7038 normalizedQuery
= normalizeForMatching( query
);
7040 // Support deprecated exact=true argument
7041 if ( mode
=== true ) {
7045 return function ( item
) {
7046 var matchText
= normalizeForMatching( item
.getMatchText() );
7048 if ( normalizedQuery
=== '' ) {
7049 // Empty string matches all, except if we are in 'exact'
7050 // mode, where it doesn't match at all
7051 return mode
!== 'exact';
7056 return matchText
=== normalizedQuery
;
7058 return matchText
.indexOf( normalizedQuery
) !== -1;
7061 return matchText
.indexOf( normalizedQuery
) === 0;
7067 * Bind document key press listener.
7071 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7072 this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
7076 * Unbind document key down listener.
7078 * If you override this, be sure to call this.clearKeyPressBuffer() from your
7083 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7084 this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
7085 this.clearKeyPressBuffer();
7089 * Visibility change handler
7092 * @param {boolean} visible
7094 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
7096 this.clearKeyPressBuffer();
7101 * Get the closest item to a jQuery.Event.
7104 * @param {jQuery.Event} e
7105 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
7107 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
7108 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
7109 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
7112 return $option
.data( 'oo-ui-optionWidget' ) || null;
7116 * Find all selected items, if there are any. If the widget allows for multiselect
7117 * it will return an array of selected options. If the widget doesn't allow for
7118 * multiselect, it will return the selected option or null if no item is selected.
7120 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
7121 * then return an array of selected items (or empty array),
7122 * if the widget is not multiselect, return a single selected item, or `null`
7123 * if no item is selected
7125 OO
.ui
.SelectWidget
.prototype.findSelectedItems = function () {
7126 var selected
= this.items
.filter( function ( item
) {
7127 return item
.isSelected();
7130 return this.multiselect
?
7132 selected
[ 0 ] || null;
7136 * Find selected item.
7138 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
7139 * then return an array of selected items (or empty array),
7140 * if the widget is not multiselect, return a single selected item, or `null`
7141 * if no item is selected
7143 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
7144 return this.findSelectedItems();
7148 * Find highlighted item.
7150 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
7152 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
7155 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7156 if ( this.items
[ i
].isHighlighted() ) {
7157 return this.items
[ i
];
7164 * Toggle pressed state.
7166 * Press is a state that occurs when a user mouses down on an item, but
7167 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
7168 * until the user releases the mouse.
7170 * @param {boolean} pressed An option is being pressed
7172 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
7173 if ( pressed
=== undefined ) {
7174 pressed
= !this.pressed
;
7176 if ( pressed
!== this.pressed
) {
7178 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
7179 .toggleClass( 'oo-ui-selectWidget-unpressed', !pressed
);
7180 this.pressed
= pressed
;
7185 * Highlight an option. If the `item` param is omitted, no options will be highlighted
7186 * and any existing highlight will be removed. The highlight is mutually exclusive.
7188 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
7191 * @return {OO.ui.Widget} The widget, for chaining
7193 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
7194 var i
, len
, highlighted
,
7197 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7198 highlighted
= this.items
[ i
] === item
;
7199 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
7200 this.items
[ i
].setHighlighted( highlighted
);
7206 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7208 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7210 this.emit( 'highlight', item
);
7217 * Fetch an item by its label.
7219 * @param {string} label Label of the item to select.
7220 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7221 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7223 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
7225 len
= this.items
.length
,
7226 filter
= this.getItemMatcher( label
, 'exact' );
7228 for ( i
= 0; i
< len
; i
++ ) {
7229 item
= this.items
[ i
];
7230 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7237 filter
= this.getItemMatcher( label
, 'prefix' );
7238 for ( i
= 0; i
< len
; i
++ ) {
7239 item
= this.items
[ i
];
7240 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7256 * Programmatically select an option by its label. If the item does not exist,
7257 * all options will be deselected.
7259 * @param {string} [label] Label of the item to select.
7260 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7263 * @return {OO.ui.Widget} The widget, for chaining
7265 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
7266 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
7267 if ( label
=== undefined || !itemFromLabel
) {
7268 return this.selectItem();
7270 return this.selectItem( itemFromLabel
);
7274 * Programmatically select an option by its data. If the `data` parameter is omitted,
7275 * or if the item does not exist, all options will be deselected.
7277 * @param {Object|string} [data] Value of the item to select, omit to deselect all
7280 * @return {OO.ui.Widget} The widget, for chaining
7282 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
7283 var itemFromData
= this.findItemFromData( data
);
7284 if ( data
=== undefined || !itemFromData
) {
7285 return this.selectItem();
7287 return this.selectItem( itemFromData
);
7291 * Programmatically unselect an option by its reference. If the widget
7292 * allows for multiple selections, there may be other items still selected;
7293 * otherwise, no items will be selected.
7294 * If no item is given, all selected items will be unselected.
7296 * @param {OO.ui.OptionWidget} [item] Item to unselect
7299 * @return {OO.ui.Widget} The widget, for chaining
7301 OO
.ui
.SelectWidget
.prototype.unselectItem = function ( item
) {
7303 item
.setSelected( false );
7305 this.items
.forEach( function ( item
) {
7306 item
.setSelected( false );
7310 this.emit( 'select', this.findSelectedItems() );
7315 * Programmatically select an option by its reference. If the `item` parameter is omitted,
7316 * all options will be deselected.
7318 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7321 * @return {OO.ui.Widget} The widget, for chaining
7323 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
7324 var i
, len
, selected
,
7327 if ( this.multiselect
&& item
) {
7328 // Select the item directly
7329 item
.setSelected( true );
7331 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7332 selected
= this.items
[ i
] === item
;
7333 if ( this.items
[ i
].isSelected() !== selected
) {
7334 this.items
[ i
].setSelected( selected
);
7340 // TODO: When should a non-highlightable element be selected?
7341 if ( item
&& !item
.constructor.static.highlightable
) {
7343 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7345 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7348 this.emit( 'select', this.findSelectedItems() );
7357 * Press is a state that occurs when a user mouses down on an item, but has not
7358 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7359 * releases the mouse.
7361 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7364 * @return {OO.ui.Widget} The widget, for chaining
7366 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
7367 var i
, len
, pressed
,
7370 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7371 pressed
= this.items
[ i
] === item
;
7372 if ( this.items
[ i
].isPressed() !== pressed
) {
7373 this.items
[ i
].setPressed( pressed
);
7378 this.emit( 'press', item
);
7387 * Note that ‘choose’ should never be modified programmatically. A user can choose
7388 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7389 * use the #selectItem method.
7391 * This method is identical to #selectItem, but may vary in subclasses that take additional action
7392 * when users choose an item with the keyboard or mouse.
7394 * @param {OO.ui.OptionWidget} item Item to choose
7397 * @return {OO.ui.Widget} The widget, for chaining
7399 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
7401 if ( this.multiselect
&& item
.isSelected() ) {
7402 this.unselectItem( item
);
7404 this.selectItem( item
);
7407 this.emit( 'choose', item
, item
.isSelected() );
7414 * Find an option by its position relative to the specified item (or to the start of the option
7415 * array, if item is `null`). The direction in which to search through the option array is specified
7416 * with a number: -1 for reverse (the default) or 1 for forward. The method will return an option,
7417 * or `null` if there are no options in the array.
7419 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at
7420 * the beginning of the array.
7421 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7422 * @param {Function} [filter] Only consider items for which this function returns
7423 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
7424 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
7426 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
7427 var currentIndex
, nextIndex
, i
,
7428 increase
= direction
> 0 ? 1 : -1,
7429 len
= this.items
.length
;
7431 if ( item
instanceof OO
.ui
.OptionWidget
) {
7432 currentIndex
= this.items
.indexOf( item
);
7433 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7435 // If no item is selected and moving forward, start at the beginning.
7436 // If moving backward, start at the end.
7437 nextIndex
= direction
> 0 ? 0 : len
- 1;
7440 for ( i
= 0; i
< len
; i
++ ) {
7441 item
= this.items
[ nextIndex
];
7443 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7444 ( !filter
|| filter( item
) )
7448 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7454 * Find the next selectable item or `null` if there are no selectable items.
7455 * Disabled options and menu-section markers and breaks are not selectable.
7457 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7459 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7460 return this.findRelativeSelectableItem( null, 1 );
7464 * Add an array of options to the select. Optionally, an index number can be used to
7465 * specify an insertion point.
7467 * @param {OO.ui.OptionWidget[]} items Items to add
7468 * @param {number} [index] Index to insert items after
7471 * @return {OO.ui.Widget} The widget, for chaining
7473 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7475 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7477 // Always provide an index, even if it was omitted
7478 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7484 * Remove the specified array of options from the select. Options will be detached
7485 * from the DOM, not removed, so they can be reused later. To remove all options from
7486 * the select, you may wish to use the #clearItems method instead.
7488 * @param {OO.ui.OptionWidget[]} items Items to remove
7491 * @return {OO.ui.Widget} The widget, for chaining
7493 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7496 // Deselect items being removed
7497 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7499 if ( item
.isSelected() ) {
7500 this.selectItem( null );
7505 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7507 this.emit( 'remove', items
);
7513 * Clear all options from the select. Options will be detached from the DOM, not removed,
7514 * so that they can be reused later. To remove a subset of options from the select, use
7515 * the #removeItems method.
7519 * @return {OO.ui.Widget} The widget, for chaining
7521 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7522 var items
= this.items
.slice();
7525 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7528 this.selectItem( null );
7530 this.emit( 'remove', items
);
7536 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7538 * This is used to set `aria-activedescendant` and `aria-expanded` on it.
7541 * @param {jQuery} $focusOwner
7543 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7544 this.$focusOwner
= $focusOwner
;
7548 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7549 * with an {@link OO.ui.mixin.IconElement icon} and/or
7550 * {@link OO.ui.mixin.IndicatorElement indicator}.
7551 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7552 * options. For more information about options and selects, please see the
7553 * [OOUI documentation on MediaWiki][1].
7556 * // Decorated options in a select widget.
7557 * var select = new OO.ui.SelectWidget( {
7559 * new OO.ui.DecoratedOptionWidget( {
7561 * label: 'Option with icon',
7564 * new OO.ui.DecoratedOptionWidget( {
7566 * label: 'Option with indicator',
7571 * $( document.body ).append( select.$element );
7573 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7576 * @extends OO.ui.OptionWidget
7577 * @mixins OO.ui.mixin.IconElement
7578 * @mixins OO.ui.mixin.IndicatorElement
7581 * @param {Object} [config] Configuration options
7583 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7584 // Parent constructor
7585 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7587 // Mixin constructors
7588 OO
.ui
.mixin
.IconElement
.call( this, config
);
7589 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7593 .addClass( 'oo-ui-decoratedOptionWidget' )
7594 .prepend( this.$icon
)
7595 .append( this.$indicator
);
7600 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7601 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7602 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7605 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7606 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7607 * the [OOUI documentation on MediaWiki] [1] for more information.
7609 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7612 * @extends OO.ui.DecoratedOptionWidget
7615 * @param {Object} [config] Configuration options
7617 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7618 // Parent constructor
7619 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7622 this.checkIcon
= new OO
.ui
.IconWidget( {
7624 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7629 .prepend( this.checkIcon
.$element
)
7630 .addClass( 'oo-ui-menuOptionWidget' );
7635 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7637 /* Static Properties */
7643 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7646 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to
7647 * group one or more related {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets
7648 * cannot be highlighted or selected.
7651 * var dropdown = new OO.ui.DropdownWidget( {
7654 * new OO.ui.MenuSectionOptionWidget( {
7657 * new OO.ui.MenuOptionWidget( {
7659 * label: 'Welsh Corgi'
7661 * new OO.ui.MenuOptionWidget( {
7663 * label: 'Standard Poodle'
7665 * new OO.ui.MenuSectionOptionWidget( {
7668 * new OO.ui.MenuOptionWidget( {
7675 * $( document.body ).append( dropdown.$element );
7678 * @extends OO.ui.DecoratedOptionWidget
7681 * @param {Object} [config] Configuration options
7683 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7684 // Parent constructor
7685 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7689 .addClass( 'oo-ui-menuSectionOptionWidget' )
7690 .removeAttr( 'role aria-selected' );
7691 this.selected
= false;
7696 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7698 /* Static Properties */
7704 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7710 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7713 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7714 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7715 * See {@link OO.ui.DropdownWidget DropdownWidget},
7716 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}, and
7717 * {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7718 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7719 * and customized to be opened, closed, and displayed as needed.
7721 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7722 * mouse outside the menu.
7724 * Menus also have support for keyboard interaction:
7726 * - Enter/Return key: choose and select a menu option
7727 * - Up-arrow key: highlight the previous menu option
7728 * - Down-arrow key: highlight the next menu option
7729 * - Escape key: hide the menu
7731 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7733 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7734 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7737 * @extends OO.ui.SelectWidget
7738 * @mixins OO.ui.mixin.ClippableElement
7739 * @mixins OO.ui.mixin.FloatableElement
7742 * @param {Object} [config] Configuration options
7743 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu
7744 * items that match the text the user types. This config is used by
7745 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget} and
7746 * {@link OO.ui.mixin.LookupElement LookupElement}
7747 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7748 * the text the user types. This config is used by
7749 * {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
7750 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks
7751 * the mouse anywhere on the page outside of this widget, the menu is hidden. For example, if
7752 * there is a button that toggles the menu's visibility on click, the menu will be hidden then
7753 * re-shown when the user clicks that button, unless the button (or its parent widget) is passed
7755 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7756 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7757 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7758 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7759 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7760 * @cfg {string} [filterMode='prefix'] The mode by which the menu filters the results.
7761 * Options are 'exact', 'prefix' or 'substring'. See `OO.ui.SelectWidget#getItemMatcher`
7762 * @cfg {number|string} [width] Width of the menu as a number of pixels or CSS string with unit
7763 * suffix, used by {@link OO.ui.mixin.ClippableElement ClippableElement}
7765 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7766 // Configuration initialization
7767 config
= config
|| {};
7769 // Parent constructor
7770 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7772 // Mixin constructors
7773 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( { $clippable
: this.$group
}, config
) );
7774 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7776 // Initial vertical positions other than 'center' will result in
7777 // the menu being flipped if there is not enough space in the container.
7778 // Store the original position so we know what to reset to.
7779 this.originalVerticalPosition
= this.verticalPosition
;
7782 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7783 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7784 this.filterFromInput
= !!config
.filterFromInput
;
7785 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7786 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7787 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7788 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7789 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7790 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7791 this.lastHighlightedItem
= null;
7792 this.width
= config
.width
;
7793 this.filterMode
= config
.filterMode
;
7796 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7797 if ( config
.widget
) {
7798 this.setFocusOwner( config
.widget
.$tabIndexed
);
7801 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7802 // that reference properties not initialized at that time of parent class construction
7803 // TODO: Find a better way to handle post-constructor setup
7804 this.visible
= false;
7805 this.$element
.addClass( 'oo-ui-element-hidden' );
7806 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7811 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7812 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7813 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7820 * The menu is ready: it is visible and has been positioned and clipped.
7823 /* Static properties */
7826 * Positions to flip to if there isn't room in the container for the
7827 * menu in a specific direction.
7829 * @property {Object.<string,string>}
7831 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7841 * Handles document mouse down events.
7844 * @param {MouseEvent} e Mouse down event
7846 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7850 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7855 this.toggle( false );
7862 OO
.ui
.MenuSelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
7863 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7865 if ( !this.isDisabled() && this.isVisible() ) {
7866 switch ( e
.keyCode
) {
7867 case OO
.ui
.Keys
.LEFT
:
7868 case OO
.ui
.Keys
.RIGHT
:
7869 // Do nothing if a text field is associated, arrow keys will be handled natively
7870 if ( !this.$input
) {
7871 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7874 case OO
.ui
.Keys
.ESCAPE
:
7875 case OO
.ui
.Keys
.TAB
:
7876 if ( currentItem
&& !this.multiselect
) {
7877 currentItem
.setHighlighted( false );
7879 this.toggle( false );
7880 // Don't prevent tabbing away, prevent defocusing
7881 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7883 e
.stopPropagation();
7887 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7894 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7895 * or after items were added/removed (always).
7899 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7900 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7902 len
= this.items
.length
,
7903 showAll
= !this.isVisible(),
7906 if ( this.$input
&& this.filterFromInput
) {
7907 filter
= showAll
? null : this.getItemMatcher( this.$input
.val(), this.filterMode
);
7908 exactFilter
= this.getItemMatcher( this.$input
.val(), 'exact' );
7909 // Hide non-matching options, and also hide section headers if all options
7910 // in their section are hidden.
7911 for ( i
= 0; i
< len
; i
++ ) {
7912 item
= this.items
[ i
];
7913 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7915 // If the previous section was empty, hide its header
7916 section
.toggle( showAll
|| !sectionEmpty
);
7919 sectionEmpty
= true;
7920 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7921 visible
= showAll
|| filter( item
);
7922 exactMatch
= exactMatch
|| exactFilter( item
);
7923 anyVisible
= anyVisible
|| visible
;
7924 sectionEmpty
= sectionEmpty
&& !visible
;
7925 item
.toggle( visible
);
7928 // Process the final section
7930 section
.toggle( showAll
|| !sectionEmpty
);
7933 if ( !anyVisible
) {
7934 this.highlightItem( null );
7937 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7940 this.highlightOnFilter
&&
7941 !( this.lastHighlightedItem
&& this.lastHighlightedItem
.isVisible() ) &&
7944 // Highlight the first item on the list
7946 items
= this.getItems();
7947 for ( i
= 0; i
< items
.length
; i
++ ) {
7948 if ( items
[ i
].isVisible() ) {
7953 this.highlightItem( item
);
7954 this.lastHighlightedItem
= item
;
7958 // Reevaluate clipping
7965 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyDownListener = function () {
7966 if ( this.$input
) {
7967 this.$input
.on( 'keydown', this.onDocumentKeyDownHandler
);
7969 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyDownListener
.call( this );
7976 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
7977 if ( this.$input
) {
7978 this.$input
.off( 'keydown', this.onDocumentKeyDownHandler
);
7980 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyDownListener
.call( this );
7987 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7988 if ( this.$input
) {
7989 if ( this.filterFromInput
) {
7991 'keydown mouseup cut paste change input select',
7992 this.onInputEditHandler
7994 this.updateItemVisibility();
7997 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyPressListener
.call( this );
8004 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
8005 if ( this.$input
) {
8006 if ( this.filterFromInput
) {
8008 'keydown mouseup cut paste change input select',
8009 this.onInputEditHandler
8011 this.updateItemVisibility();
8014 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyPressListener
.call( this );
8021 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is
8024 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with
8025 * the keyboard or mouse and it becomes selected. To select an item programmatically,
8026 * use the #selectItem method.
8028 * @param {OO.ui.OptionWidget} item Item to choose
8030 * @return {OO.ui.Widget} The widget, for chaining
8032 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
8033 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
8034 if ( this.hideOnChoose
) {
8035 this.toggle( false );
8043 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
8045 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
8047 this.updateItemVisibility();
8055 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
8057 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
8059 this.updateItemVisibility();
8067 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
8069 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
8071 this.updateItemVisibility();
8077 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
8078 * `.toggle( true )` after its #$element is attached to the DOM.
8080 * Do not show the menu while it is not attached to the DOM. The calculations required to display
8081 * it in the right place and with the right dimensions only work correctly while it is attached.
8082 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
8083 * strictly enforced, so currently it only generates a warning in the browser console.
8088 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
8089 var change
, originalHeight
, flippedHeight
, selectedItem
;
8091 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
8092 change
= visible
!== this.isVisible();
8094 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
8095 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
8096 this.warnedUnattached
= true;
8099 if ( change
&& visible
) {
8100 // Reset position before showing the popup again. It's possible we no longer need to flip
8101 // (e.g. if the user scrolled).
8102 this.setVerticalPosition( this.originalVerticalPosition
);
8106 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
8112 this.setIdealSize( this.width
);
8113 } else if ( this.$floatableContainer
) {
8114 this.$clippable
.css( 'width', 'auto' );
8116 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
8117 // Dropdown is smaller than handle so expand to width
8118 this.$floatableContainer
[ 0 ].offsetWidth
:
8119 // Dropdown is larger than handle so auto size
8122 this.$clippable
.css( 'width', '' );
8125 this.togglePositioning( !!this.$floatableContainer
);
8126 this.toggleClipping( true );
8128 this.bindDocumentKeyDownListener();
8129 this.bindDocumentKeyPressListener();
8132 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
8133 this.originalVerticalPosition
!== 'center'
8135 // If opening the menu in one direction causes it to be clipped, flip it
8136 originalHeight
= this.$element
.height();
8137 this.setVerticalPosition(
8138 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
8140 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
8141 // If flipping also causes it to be clipped, open in whichever direction
8142 // we have more space
8143 flippedHeight
= this.$element
.height();
8144 if ( originalHeight
> flippedHeight
) {
8145 this.setVerticalPosition( this.originalVerticalPosition
);
8149 // Note that we do not flip the menu's opening direction if the clipping changes
8150 // later (e.g. after the user scrolls), that seems like it would be annoying
8152 this.$focusOwner
.attr( 'aria-expanded', 'true' );
8154 selectedItem
= this.findSelectedItem();
8155 if ( !this.multiselect
&& selectedItem
) {
8156 // TODO: Verify if this is even needed; This is already done on highlight changes
8157 // in SelectWidget#highlightItem, so we should just need to highlight the item
8158 // we need to highlight here and not bother with attr or checking selections.
8159 this.$focusOwner
.attr( 'aria-activedescendant', selectedItem
.getElementId() );
8160 selectedItem
.scrollElementIntoView( { duration
: 0 } );
8164 if ( this.autoHide
) {
8165 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8168 this.emit( 'ready' );
8170 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
8171 this.unbindDocumentKeyDownListener();
8172 this.unbindDocumentKeyPressListener();
8173 this.$focusOwner
.attr( 'aria-expanded', 'false' );
8174 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8175 this.togglePositioning( false );
8176 this.toggleClipping( false );
8177 this.lastHighlightedItem
= null;
8185 * Scroll to the top of the menu
8187 OO
.ui
.MenuSelectWidget
.prototype.scrollToTop = function () {
8188 this.$element
.scrollTop( 0 );
8192 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
8193 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
8194 * users can interact with it.
8196 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8197 * OO.ui.DropdownInputWidget instead.
8200 * // A DropdownWidget with a menu that contains three options.
8201 * var dropDown = new OO.ui.DropdownWidget( {
8202 * label: 'Dropdown menu: Select a menu option',
8205 * new OO.ui.MenuOptionWidget( {
8209 * new OO.ui.MenuOptionWidget( {
8213 * new OO.ui.MenuOptionWidget( {
8221 * $( document.body ).append( dropDown.$element );
8223 * dropDown.getMenu().selectItemByData( 'b' );
8225 * dropDown.getMenu().findSelectedItem().getData(); // Returns 'b'.
8227 * For more information, please see the [OOUI documentation on MediaWiki] [1].
8229 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8232 * @extends OO.ui.Widget
8233 * @mixins OO.ui.mixin.IconElement
8234 * @mixins OO.ui.mixin.IndicatorElement
8235 * @mixins OO.ui.mixin.LabelElement
8236 * @mixins OO.ui.mixin.TitledElement
8237 * @mixins OO.ui.mixin.TabIndexedElement
8240 * @param {Object} [config] Configuration options
8241 * @cfg {Object} [menu] Configuration options to pass to
8242 * {@link OO.ui.MenuSelectWidget menu select widget}.
8243 * @cfg {jQuery|boolean} [$overlay] Render the menu into a separate layer. This configuration is
8244 * useful in cases where the expanded menu is larger than its containing `<div>`. The specified
8245 * overlay layer is usually on top of the containing `<div>` and has a larger area. By default,
8246 * the menu uses relative positioning. Pass 'true' to use the default overlay.
8247 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
8249 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
8250 // Configuration initialization
8251 config
= $.extend( { indicator
: 'down' }, config
);
8253 // Parent constructor
8254 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
8256 // Properties (must be set before TabIndexedElement constructor call)
8257 this.$handle
= $( '<span>' );
8258 this.$overlay
= ( config
.$overlay
=== true ?
8259 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
8261 // Mixin constructors
8262 OO
.ui
.mixin
.IconElement
.call( this, config
);
8263 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8264 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8265 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
8266 $titled
: this.$label
8268 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
8269 $tabIndexed
: this.$handle
8273 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
8275 $floatableContainer
: this.$element
8280 click
: this.onClick
.bind( this ),
8281 keydown
: this.onKeyDown
.bind( this ),
8282 // Hack? Handle type-to-search when menu is not expanded and not handling its own events.
8283 keypress
: this.menu
.onDocumentKeyPressHandler
,
8284 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
8286 this.menu
.connect( this, {
8287 select
: 'onMenuSelect',
8288 toggle
: 'onMenuToggle'
8295 'aria-readonly': 'true'
8298 .addClass( 'oo-ui-dropdownWidget-handle' )
8299 .append( this.$icon
, this.$label
, this.$indicator
)
8302 'aria-autocomplete': 'list',
8303 'aria-expanded': 'false',
8304 'aria-haspopup': 'true',
8305 'aria-owns': this.menu
.getElementId()
8308 .addClass( 'oo-ui-dropdownWidget' )
8309 .append( this.$handle
);
8310 this.$overlay
.append( this.menu
.$element
);
8315 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8316 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8317 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8318 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8319 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8320 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8327 * @return {OO.ui.MenuSelectWidget} Menu of widget
8329 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8334 * Handles menu select events.
8337 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8339 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8343 this.setLabel( null );
8347 selectedLabel
= item
.getLabel();
8349 // If the label is a DOM element, clone it, because setLabel will append() it
8350 if ( selectedLabel
instanceof $ ) {
8351 selectedLabel
= selectedLabel
.clone();
8354 this.setLabel( selectedLabel
);
8358 * Handle menu toggle events.
8361 * @param {boolean} isVisible Open state of the menu
8363 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8364 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8368 * Handle mouse click events.
8371 * @param {jQuery.Event} e Mouse click event
8372 * @return {undefined|boolean} False to prevent default if event is handled
8374 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8375 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8382 * Handle key down events.
8385 * @param {jQuery.Event} e Key down event
8386 * @return {undefined|boolean} False to prevent default if event is handled
8388 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8390 !this.isDisabled() &&
8392 e
.which
=== OO
.ui
.Keys
.ENTER
||
8394 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8395 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8396 // Space only closes the menu is the user is not typing to search.
8397 this.menu
.keyPressBuffer
=== ''
8400 !this.menu
.isVisible() &&
8402 e
.which
=== OO
.ui
.Keys
.UP
||
8403 e
.which
=== OO
.ui
.Keys
.DOWN
8414 * RadioOptionWidget is an option widget that looks like a radio button.
8415 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8416 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8418 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8421 * @extends OO.ui.OptionWidget
8424 * @param {Object} [config] Configuration options
8426 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8427 // Configuration initialization
8428 config
= config
|| {};
8430 // Properties (must be done before parent constructor which calls #setDisabled)
8431 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8433 // Parent constructor
8434 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8437 // Remove implicit role, we're handling it ourselves
8438 this.radio
.$input
.attr( 'role', 'presentation' );
8440 .addClass( 'oo-ui-radioOptionWidget' )
8441 .attr( 'role', 'radio' )
8442 .attr( 'aria-checked', 'false' )
8443 .removeAttr( 'aria-selected' )
8444 .prepend( this.radio
.$element
);
8449 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8451 /* Static Properties */
8457 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8463 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8469 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8475 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8482 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8483 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8485 this.radio
.setSelected( state
);
8487 .attr( 'aria-checked', state
.toString() )
8488 .removeAttr( 'aria-selected' );
8496 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8497 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8499 this.radio
.setDisabled( this.isDisabled() );
8505 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8506 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8507 * an interface for adding, removing and selecting options.
8508 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8510 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8511 * OO.ui.RadioSelectInputWidget instead.
8514 * // A RadioSelectWidget with RadioOptions.
8515 * var option1 = new OO.ui.RadioOptionWidget( {
8517 * label: 'Selected radio option'
8519 * option2 = new OO.ui.RadioOptionWidget( {
8521 * label: 'Unselected radio option'
8523 * radioSelect = new OO.ui.RadioSelectWidget( {
8524 * items: [ option1, option2 ]
8527 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8528 * radioSelect.selectItem( option1 );
8530 * $( document.body ).append( radioSelect.$element );
8532 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8536 * @extends OO.ui.SelectWidget
8537 * @mixins OO.ui.mixin.TabIndexedElement
8540 * @param {Object} [config] Configuration options
8542 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8543 // Parent constructor
8544 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8546 // Mixin constructors
8547 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8551 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8552 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8557 .addClass( 'oo-ui-radioSelectWidget' )
8558 .attr( 'role', 'radiogroup' );
8563 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8564 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8567 * MultioptionWidgets are special elements that can be selected and configured with data. The
8568 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8569 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8570 * and examples, please see the [OOUI documentation on MediaWiki][1].
8572 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8575 * @extends OO.ui.Widget
8576 * @mixins OO.ui.mixin.ItemWidget
8577 * @mixins OO.ui.mixin.LabelElement
8578 * @mixins OO.ui.mixin.TitledElement
8581 * @param {Object} [config] Configuration options
8582 * @cfg {boolean} [selected=false] Whether the option is initially selected
8584 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8585 // Configuration initialization
8586 config
= config
|| {};
8588 // Parent constructor
8589 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8591 // Mixin constructors
8592 OO
.ui
.mixin
.ItemWidget
.call( this );
8593 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8594 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8597 this.selected
= null;
8601 .addClass( 'oo-ui-multioptionWidget' )
8602 .append( this.$label
);
8603 this.setSelected( config
.selected
);
8608 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8609 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8610 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8611 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.TitledElement
);
8618 * A change event is emitted when the selected state of the option changes.
8620 * @param {boolean} selected Whether the option is now selected
8626 * Check if the option is selected.
8628 * @return {boolean} Item is selected
8630 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8631 return this.selected
;
8635 * Set the option’s selected state. In general, all modifications to the selection
8636 * should be handled by the SelectWidget’s
8637 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
8639 * @param {boolean} [state=false] Select option
8641 * @return {OO.ui.Widget} The widget, for chaining
8643 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8645 if ( this.selected
!== state
) {
8646 this.selected
= state
;
8647 this.emit( 'change', state
);
8648 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8654 * MultiselectWidget allows selecting multiple options from a list.
8656 * For more information about menus and options, please see the [OOUI documentation
8659 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8663 * @extends OO.ui.Widget
8664 * @mixins OO.ui.mixin.GroupWidget
8665 * @mixins OO.ui.mixin.TitledElement
8668 * @param {Object} [config] Configuration options
8669 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8671 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8672 // Parent constructor
8673 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8675 // Configuration initialization
8676 config
= config
|| {};
8678 // Mixin constructors
8679 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8680 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8686 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8687 // by GroupElement only when items are added/removed
8688 this.connect( this, {
8689 select
: [ 'emit', 'change' ]
8693 if ( config
.items
) {
8694 this.addItems( config
.items
);
8696 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8697 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8698 .append( this.$group
);
8703 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8704 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8705 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.TitledElement
);
8712 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8718 * A select event is emitted when an item is selected or deselected.
8724 * Find options that are selected.
8726 * @return {OO.ui.MultioptionWidget[]} Selected options
8728 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8729 return this.items
.filter( function ( item
) {
8730 return item
.isSelected();
8735 * Find the data of options that are selected.
8737 * @return {Object[]|string[]} Values of selected options
8739 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8740 return this.findSelectedItems().map( function ( item
) {
8746 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8748 * @param {OO.ui.MultioptionWidget[]} items Items to select
8750 * @return {OO.ui.Widget} The widget, for chaining
8752 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8753 this.items
.forEach( function ( item
) {
8754 var selected
= items
.indexOf( item
) !== -1;
8755 item
.setSelected( selected
);
8761 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8763 * @param {Object[]|string[]} datas Values of items to select
8765 * @return {OO.ui.Widget} The widget, for chaining
8767 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8770 items
= datas
.map( function ( data
) {
8771 return widget
.findItemFromData( data
);
8773 this.selectItems( items
);
8778 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8779 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8780 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8782 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8785 * @extends OO.ui.MultioptionWidget
8788 * @param {Object} [config] Configuration options
8790 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8791 // Configuration initialization
8792 config
= config
|| {};
8794 // Properties (must be done before parent constructor which calls #setDisabled)
8795 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8797 // Parent constructor
8798 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8801 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8802 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8806 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8807 .prepend( this.checkbox
.$element
);
8812 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8814 /* Static Properties */
8820 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8825 * Handle checkbox selected state change.
8829 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8830 this.setSelected( this.checkbox
.isSelected() );
8836 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8837 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8838 this.checkbox
.setSelected( state
);
8845 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8846 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8847 this.checkbox
.setDisabled( this.isDisabled() );
8854 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8855 this.checkbox
.focus();
8859 * Handle key down events.
8862 * @param {jQuery.Event} e
8864 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8866 element
= this.getElementGroup(),
8869 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8870 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8871 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8872 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8882 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8883 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8884 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8885 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8887 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8888 * OO.ui.CheckboxMultiselectInputWidget instead.
8891 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8892 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8895 * label: 'Selected checkbox'
8897 * option2 = new OO.ui.CheckboxMultioptionWidget( {
8899 * label: 'Unselected checkbox'
8901 * multiselect = new OO.ui.CheckboxMultiselectWidget( {
8902 * items: [ option1, option2 ]
8904 * $( document.body ).append( multiselect.$element );
8906 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8909 * @extends OO.ui.MultiselectWidget
8912 * @param {Object} [config] Configuration options
8914 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8915 // Parent constructor
8916 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8919 this.$lastClicked
= null;
8922 this.$group
.on( 'click', this.onClick
.bind( this ) );
8925 this.$element
.addClass( 'oo-ui-checkboxMultiselectWidget' );
8930 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8935 * Get an option by its position relative to the specified item (or to the start of the
8936 * option array, if item is `null`). The direction in which to search through the option array
8937 * is specified with a number: -1 for reverse (the default) or 1 for forward. The method will
8938 * return an option, or `null` if there are no options in the array.
8940 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or
8941 * `null` to start at the beginning of the array.
8942 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8943 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items
8946 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8947 var currentIndex
, nextIndex
, i
,
8948 increase
= direction
> 0 ? 1 : -1,
8949 len
= this.items
.length
;
8952 currentIndex
= this.items
.indexOf( item
);
8953 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8955 // If no item is selected and moving forward, start at the beginning.
8956 // If moving backward, start at the end.
8957 nextIndex
= direction
> 0 ? 0 : len
- 1;
8960 for ( i
= 0; i
< len
; i
++ ) {
8961 item
= this.items
[ nextIndex
];
8962 if ( item
&& !item
.isDisabled() ) {
8965 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8971 * Handle click events on checkboxes.
8973 * @param {jQuery.Event} e
8975 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8976 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8977 $lastClicked
= this.$lastClicked
,
8978 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8979 .not( '.oo-ui-widget-disabled' );
8981 // Allow selecting multiple options at once by Shift-clicking them
8982 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8983 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8984 lastClickedIndex
= $options
.index( $lastClicked
);
8985 nowClickedIndex
= $options
.index( $nowClicked
);
8986 // If it's the same item, either the user is being silly, or it's a fake event generated
8987 // by the browser. In either case we don't need custom handling.
8988 if ( nowClickedIndex
!== lastClickedIndex
) {
8990 wasSelected
= items
[ nowClickedIndex
].isSelected();
8991 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8993 // This depends on the DOM order of the items and the order of the .items array being
8995 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8996 if ( !items
[ i
].isDisabled() ) {
8997 items
[ i
].setSelected( !wasSelected
);
9000 // For the now-clicked element, use immediate timeout to allow the browser to do its own
9001 // handling first, then set our value. The order in which events happen is different for
9002 // clicks on the <input> and on the <label> and there are additional fake clicks fired
9003 // for non-click actions that change the checkboxes.
9005 setTimeout( function () {
9006 if ( !items
[ nowClickedIndex
].isDisabled() ) {
9007 items
[ nowClickedIndex
].setSelected( !wasSelected
);
9013 if ( $nowClicked
.length
) {
9014 this.$lastClicked
= $nowClicked
;
9022 * @return {OO.ui.Widget} The widget, for chaining
9024 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
9026 if ( !this.isDisabled() ) {
9027 item
= this.getRelativeFocusableItem( null, 1 );
9038 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
9043 * Progress bars visually display the status of an operation, such as a download,
9044 * and can be either determinate or indeterminate:
9046 * - **determinate** process bars show the percent of an operation that is complete.
9048 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
9049 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
9050 * not use percentages.
9052 * The value of the `progress` configuration determines whether the bar is determinate
9056 * // Examples of determinate and indeterminate progress bars.
9057 * var progressBar1 = new OO.ui.ProgressBarWidget( {
9060 * var progressBar2 = new OO.ui.ProgressBarWidget();
9062 * // Create a FieldsetLayout to layout progress bars.
9063 * var fieldset = new OO.ui.FieldsetLayout;
9064 * fieldset.addItems( [
9065 * new OO.ui.FieldLayout( progressBar1, {
9066 * label: 'Determinate',
9069 * new OO.ui.FieldLayout( progressBar2, {
9070 * label: 'Indeterminate',
9074 * $( document.body ).append( fieldset.$element );
9077 * @extends OO.ui.Widget
9080 * @param {Object} [config] Configuration options
9081 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
9082 * To create a determinate progress bar, specify a number that reflects the initial
9084 * By default, the progress bar is indeterminate.
9086 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
9087 // Configuration initialization
9088 config
= config
|| {};
9090 // Parent constructor
9091 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
9094 this.$bar
= $( '<div>' );
9095 this.progress
= null;
9098 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
9099 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
9102 role
: 'progressbar',
9104 'aria-valuemax': 100
9106 .addClass( 'oo-ui-progressBarWidget' )
9107 .append( this.$bar
);
9112 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
9114 /* Static Properties */
9120 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
9125 * Get the percent of the progress that has been completed. Indeterminate progresses will
9128 * @return {number|boolean} Progress percent
9130 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
9131 return this.progress
;
9135 * Set the percent of the process completed or `false` for an indeterminate process.
9137 * @param {number|boolean} progress Progress percent or `false` for indeterminate
9139 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
9140 this.progress
= progress
;
9142 if ( progress
!== false ) {
9143 this.$bar
.css( 'width', this.progress
+ '%' );
9144 this.$element
.attr( 'aria-valuenow', this.progress
);
9146 this.$bar
.css( 'width', '' );
9147 this.$element
.removeAttr( 'aria-valuenow' );
9149 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
9153 * InputWidget is the base class for all input widgets, which
9154 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox
9155 * inputs}, {@link OO.ui.RadioInputWidget radio inputs}, and
9156 * {@link OO.ui.ButtonInputWidget button inputs}.
9157 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
9159 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9163 * @extends OO.ui.Widget
9164 * @mixins OO.ui.mixin.TabIndexedElement
9165 * @mixins OO.ui.mixin.TitledElement
9166 * @mixins OO.ui.mixin.AccessKeyedElement
9169 * @param {Object} [config] Configuration options
9170 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9171 * @cfg {string} [value=''] The value of the input.
9172 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
9173 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
9174 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the
9175 * value of an input before it is accepted.
9177 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
9178 // Configuration initialization
9179 config
= config
|| {};
9181 // Parent constructor
9182 OO
.ui
.InputWidget
.parent
.call( this, config
);
9185 // See #reusePreInfuseDOM about config.$input
9186 this.$input
= config
.$input
|| this.getInputElement( config
);
9188 this.inputFilter
= config
.inputFilter
;
9190 // Mixin constructors
9191 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
9192 $tabIndexed
: this.$input
9194 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
9195 $titled
: this.$input
9197 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
9198 $accessKeyed
: this.$input
9202 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
9206 .addClass( 'oo-ui-inputWidget-input' )
9207 .attr( 'name', config
.name
)
9208 .prop( 'disabled', this.isDisabled() );
9210 .addClass( 'oo-ui-inputWidget' )
9211 .append( this.$input
);
9212 this.setValue( config
.value
);
9214 this.setDir( config
.dir
);
9216 if ( config
.inputId
!== undefined ) {
9217 this.setInputId( config
.inputId
);
9223 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
9224 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
9225 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
9226 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
9228 /* Static Methods */
9233 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9234 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9235 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
9236 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
9243 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9244 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9245 if ( config
.$input
&& config
.$input
.length
) {
9246 state
.value
= config
.$input
.val();
9247 // Might be better in TabIndexedElement, but it's awkward to do there because
9248 // mixins are awkward
9249 state
.focus
= config
.$input
.is( ':focus' );
9259 * A change event is emitted when the value of the input changes.
9261 * @param {string} value
9267 * Get input element.
9269 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9270 * different circumstances. The element must have a `value` property (like form elements).
9273 * @param {Object} config Configuration options
9274 * @return {jQuery} Input element
9276 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
9277 return $( '<input>' );
9281 * Handle potentially value-changing events.
9284 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9286 OO
.ui
.InputWidget
.prototype.onEdit = function () {
9288 if ( !this.isDisabled() ) {
9289 // Allow the stack to clear so the value will be updated
9290 setTimeout( function () {
9291 widget
.setValue( widget
.$input
.val() );
9297 * Get the value of the input.
9299 * @return {string} Input value
9301 OO
.ui
.InputWidget
.prototype.getValue = function () {
9302 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9303 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9304 var value
= this.$input
.val();
9305 if ( this.value
!== value
) {
9306 this.setValue( value
);
9312 * Set the directionality of the input.
9314 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9316 * @return {OO.ui.Widget} The widget, for chaining
9318 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9319 this.$input
.prop( 'dir', dir
);
9324 * Set the value of the input.
9326 * @param {string} value New value
9329 * @return {OO.ui.Widget} The widget, for chaining
9331 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9332 value
= this.cleanUpValue( value
);
9333 // Update the DOM if it has changed. Note that with cleanUpValue, it
9334 // is possible for the DOM value to change without this.value changing.
9335 if ( this.$input
.val() !== value
) {
9336 this.$input
.val( value
);
9338 if ( this.value
!== value
) {
9340 this.emit( 'change', this.value
);
9342 // The first time that the value is set (probably while constructing the widget),
9343 // remember it in defaultValue. This property can be later used to check whether
9344 // the value of the input has been changed since it was created.
9345 if ( this.defaultValue
=== undefined ) {
9346 this.defaultValue
= this.value
;
9347 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9353 * Clean up incoming value.
9355 * Ensures value is a string, and converts undefined and null to empty string.
9358 * @param {string} value Original value
9359 * @return {string} Cleaned up value
9361 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9362 if ( value
=== undefined || value
=== null ) {
9364 } else if ( this.inputFilter
) {
9365 return this.inputFilter( String( value
) );
9367 return String( value
);
9374 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9375 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9376 if ( this.$input
) {
9377 this.$input
.prop( 'disabled', this.isDisabled() );
9383 * Set the 'id' attribute of the `<input>` element.
9385 * @param {string} id
9387 * @return {OO.ui.Widget} The widget, for chaining
9389 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9390 this.$input
.attr( 'id', id
);
9397 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9398 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9399 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9400 this.setValue( state
.value
);
9402 if ( state
.focus
) {
9408 * Data widget intended for creating `<input type="hidden">` inputs.
9411 * @extends OO.ui.Widget
9414 * @param {Object} [config] Configuration options
9415 * @cfg {string} [value=''] The value of the input.
9416 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9418 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9419 // Configuration initialization
9420 config
= $.extend( { value
: '', name
: '' }, config
);
9422 // Parent constructor
9423 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9426 this.$element
.attr( {
9428 value
: config
.value
,
9431 this.$element
.removeAttr( 'aria-disabled' );
9436 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9438 /* Static Properties */
9444 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9447 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9448 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9449 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9450 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9451 * [OOUI documentation on MediaWiki] [1] for more information.
9454 * // A ButtonInputWidget rendered as an HTML button, the default.
9455 * var button = new OO.ui.ButtonInputWidget( {
9456 * label: 'Input button',
9460 * $( document.body ).append( button.$element );
9462 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9465 * @extends OO.ui.InputWidget
9466 * @mixins OO.ui.mixin.ButtonElement
9467 * @mixins OO.ui.mixin.IconElement
9468 * @mixins OO.ui.mixin.IndicatorElement
9469 * @mixins OO.ui.mixin.LabelElement
9470 * @mixins OO.ui.mixin.FlaggedElement
9473 * @param {Object} [config] Configuration options
9474 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute:
9475 * 'button', 'submit' or 'reset'.
9476 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9477 * Widgets configured to be an `<input>` do not support {@link #icon icons} and
9478 * {@link #indicator indicators},
9479 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should
9480 * only be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9482 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9483 // Configuration initialization
9484 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9486 // See InputWidget#reusePreInfuseDOM about config.$input
9487 if ( config
.$input
) {
9488 config
.$input
.empty();
9491 // Properties (must be set before parent constructor, which calls #setValue)
9492 this.useInputTag
= config
.useInputTag
;
9494 // Parent constructor
9495 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9497 // Mixin constructors
9498 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {
9499 $button
: this.$input
9501 OO
.ui
.mixin
.IconElement
.call( this, config
);
9502 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9503 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9504 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
9507 if ( !config
.useInputTag
) {
9508 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9510 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9515 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9516 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9517 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9518 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9519 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9520 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
9522 /* Static Properties */
9528 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9536 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9538 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9539 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9545 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9547 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9548 * text, or `null` for no label
9550 * @return {OO.ui.Widget} The widget, for chaining
9552 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9553 if ( typeof label
=== 'function' ) {
9554 label
= OO
.ui
.resolveMsg( label
);
9557 if ( this.useInputTag
) {
9558 // Discard non-plaintext labels
9559 if ( typeof label
!== 'string' ) {
9563 this.$input
.val( label
);
9566 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9570 * Set the value of the input.
9572 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9573 * they do not support {@link #value values}.
9575 * @param {string} value New value
9577 * @return {OO.ui.Widget} The widget, for chaining
9579 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9580 if ( !this.useInputTag
) {
9581 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9589 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9590 // Disable generating `<label>` elements for buttons. One would very rarely need additional
9591 // label for a button, and it's already a big clickable target, and it causes
9592 // unexpected rendering.
9597 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9598 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9599 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9600 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9602 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9605 * // An example of selected, unselected, and disabled checkbox inputs.
9606 * var checkbox1 = new OO.ui.CheckboxInputWidget( {
9610 * checkbox2 = new OO.ui.CheckboxInputWidget( {
9613 * checkbox3 = new OO.ui.CheckboxInputWidget( {
9617 * // Create a fieldset layout with fields for each checkbox.
9618 * fieldset = new OO.ui.FieldsetLayout( {
9619 * label: 'Checkboxes'
9621 * fieldset.addItems( [
9622 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9623 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9624 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9626 * $( document.body ).append( fieldset.$element );
9628 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9631 * @extends OO.ui.InputWidget
9634 * @param {Object} [config] Configuration options
9635 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is
9637 * @cfg {boolean} [indeterminate=false] Whether the checkbox is in the indeterminate state.
9639 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9640 // Configuration initialization
9641 config
= config
|| {};
9643 // Parent constructor
9644 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9647 this.checkIcon
= new OO
.ui
.IconWidget( {
9649 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9654 .addClass( 'oo-ui-checkboxInputWidget' )
9655 // Required for pretty styling in WikimediaUI theme
9656 .append( this.checkIcon
.$element
);
9657 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9658 this.setIndeterminate( config
.indeterminate
!== undefined ? config
.indeterminate
: false );
9663 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9670 * A change event is emitted when the state of the input changes.
9672 * @param {boolean} selected
9673 * @param {boolean} indeterminate
9676 /* Static Properties */
9682 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9684 /* Static Methods */
9689 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9690 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9691 state
.checked
= config
.$input
.prop( 'checked' );
9701 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9702 return $( '<input>' ).attr( 'type', 'checkbox' );
9708 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9710 if ( !this.isDisabled() ) {
9711 // Allow the stack to clear so the value will be updated
9712 setTimeout( function () {
9713 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9714 widget
.setIndeterminate( widget
.$input
.prop( 'indeterminate' ) );
9720 * Set selection state of this checkbox.
9722 * @param {boolean} state Selected state
9723 * @param {boolean} internal Used for internal calls to suppress events
9725 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9727 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
, internal ) {
9729 if ( this.selected
!== state
) {
9730 this.selected
= state
;
9731 this.$input
.prop( 'checked', this.selected
);
9733 this.setIndeterminate( false, true );
9734 this.emit( 'change', this.selected
, this.indeterminate
);
9737 // The first time that the selection state is set (probably while constructing the widget),
9738 // remember it in defaultSelected. This property can be later used to check whether
9739 // the selection state of the input has been changed since it was created.
9740 if ( this.defaultSelected
=== undefined ) {
9741 this.defaultSelected
= this.selected
;
9742 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9748 * Check if this checkbox is selected.
9750 * @return {boolean} Checkbox is selected
9752 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9753 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9754 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9755 var selected
= this.$input
.prop( 'checked' );
9756 if ( this.selected
!== selected
) {
9757 this.setSelected( selected
);
9759 return this.selected
;
9763 * Set indeterminate state of this checkbox.
9765 * @param {boolean} state Indeterminate state
9766 * @param {boolean} internal Used for internal calls to suppress events
9768 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9770 OO
.ui
.CheckboxInputWidget
.prototype.setIndeterminate = function ( state
, internal ) {
9772 if ( this.indeterminate
!== state
) {
9773 this.indeterminate
= state
;
9774 this.$input
.prop( 'indeterminate', this.indeterminate
);
9776 this.setSelected( false, true );
9777 this.emit( 'change', this.selected
, this.indeterminate
);
9784 * Check if this checkbox is selected.
9786 * @return {boolean} Checkbox is selected
9788 OO
.ui
.CheckboxInputWidget
.prototype.isIndeterminate = function () {
9789 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9790 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9791 var indeterminate
= this.$input
.prop( 'indeterminate' );
9792 if ( this.indeterminate
!== indeterminate
) {
9793 this.setIndeterminate( indeterminate
);
9795 return this.indeterminate
;
9801 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9802 if ( !this.isDisabled() ) {
9803 this.$handle
.trigger( 'click' );
9811 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9812 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9813 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9814 this.setSelected( state
.checked
);
9819 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9820 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the
9821 * value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9822 * more information about input widgets.
9824 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9825 * are no options. If no `value` configuration option is provided, the first option is selected.
9826 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9828 * This and OO.ui.RadioSelectInputWidget support similar configuration options.
9831 * // A DropdownInputWidget with three options.
9832 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9834 * { data: 'a', label: 'First' },
9835 * { data: 'b', label: 'Second', disabled: true },
9836 * { optgroup: 'Group label' },
9837 * { data: 'c', label: 'First sub-item)' }
9840 * $( document.body ).append( dropdownInput.$element );
9842 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9845 * @extends OO.ui.InputWidget
9848 * @param {Object} [config] Configuration options
9849 * @cfg {Object[]} [options=[]] Array of menu options in the format described above.
9850 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9851 * @cfg {jQuery|boolean} [$overlay] Render the menu into a separate layer. This configuration is
9852 * useful in cases where the expanded menu is larger than its containing `<div>`. The specified
9853 * overlay layer is usually on top of the containing `<div>` and has a larger area. By default,
9854 * the menu uses relative positioning. Pass 'true' to use the default overlay.
9855 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9857 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9858 // Configuration initialization
9859 config
= config
|| {};
9861 // Properties (must be done before parent constructor which calls #setDisabled)
9862 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9864 $overlay
: config
.$overlay
9868 // Set up the options before parent constructor, which uses them to validate config.value.
9869 // Use this instead of setOptions() because this.$input is not set up yet.
9870 this.setOptionsData( config
.options
|| [] );
9872 // Parent constructor
9873 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9876 this.dropdownWidget
.getMenu().connect( this, {
9877 select
: 'onMenuSelect'
9882 .addClass( 'oo-ui-dropdownInputWidget' )
9883 .append( this.dropdownWidget
.$element
);
9884 if ( OO
.ui
.isMobile() ) {
9885 this.$element
.addClass( 'oo-ui-isMobile' );
9887 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9888 this.setTitledElement( this.dropdownWidget
.$handle
);
9893 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9901 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9902 return $( '<select>' ).addClass( 'oo-ui-indicator-down' );
9906 * Handles menu select events.
9909 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9911 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9912 this.setValue( item
? item
.getData() : '' );
9918 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9920 value
= this.cleanUpValue( value
);
9921 // Only allow setting values that are actually present in the dropdown
9922 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9923 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9924 this.dropdownWidget
.getMenu().selectItem( selected
);
9925 value
= selected
? selected
.getData() : '';
9926 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9927 if ( this.optionsDirty
) {
9928 // We reached this from the constructor or from #setOptions.
9929 // We have to update the <select> element.
9930 this.updateOptionsInterface();
9938 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9939 this.dropdownWidget
.setDisabled( state
);
9940 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9945 * Set the options available for this input.
9947 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9949 * @return {OO.ui.Widget} The widget, for chaining
9951 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9952 var value
= this.getValue();
9954 this.setOptionsData( options
);
9956 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9957 // In case the previous value is no longer an available option, select the first valid one.
9958 this.setValue( value
);
9964 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9966 * This method may be called before the parent constructor, so various properties may not be
9969 * @param {Object[]} options Array of menu options (see #constructor for details).
9972 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9973 var optionWidgets
, optIndex
, opt
, previousOptgroup
, optionWidget
, optValue
,
9976 this.optionsDirty
= true;
9978 // Go through all the supplied option configs and create either
9979 // MenuSectionOption or MenuOption widgets from each.
9981 for ( optIndex
= 0; optIndex
< options
.length
; optIndex
++ ) {
9982 opt
= options
[ optIndex
];
9984 if ( opt
.optgroup
!== undefined ) {
9985 // Create a <optgroup> menu item.
9986 optionWidget
= widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9987 previousOptgroup
= optionWidget
;
9990 // Create a normal <option> menu item.
9991 optValue
= widget
.cleanUpValue( opt
.data
);
9992 optionWidget
= widget
.createMenuOptionWidget(
9994 opt
.label
!== undefined ? opt
.label
: optValue
9998 // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
10000 opt
.disabled
!== undefined ||
10001 previousOptgroup
instanceof OO
.ui
.MenuSectionOptionWidget
&&
10002 previousOptgroup
.isDisabled()
10004 optionWidget
.setDisabled( true );
10007 optionWidgets
.push( optionWidget
);
10010 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
10014 * Create a menu option widget.
10017 * @param {string} data Item data
10018 * @param {string} label Item label
10019 * @return {OO.ui.MenuOptionWidget} Option widget
10021 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
10022 return new OO
.ui
.MenuOptionWidget( {
10029 * Create a menu section option widget.
10032 * @param {string} label Section item label
10033 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
10035 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
10036 return new OO
.ui
.MenuSectionOptionWidget( {
10042 * Update the user-visible interface to match the internal list of options and value.
10044 * This method must only be called after the parent constructor.
10048 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
10050 $optionsContainer
= this.$input
,
10051 defaultValue
= this.defaultValue
,
10054 this.$input
.empty();
10056 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
10059 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
10060 $optionNode
= $( '<option>' )
10061 .attr( 'value', optionWidget
.getData() )
10062 .text( optionWidget
.getLabel() );
10064 // Remember original selection state. This property can be later used to check whether
10065 // the selection state of the input has been changed since it was created.
10066 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
10068 $optionsContainer
.append( $optionNode
);
10070 $optionNode
= $( '<optgroup>' )
10071 .attr( 'label', optionWidget
.getLabel() );
10072 widget
.$input
.append( $optionNode
);
10073 $optionsContainer
= $optionNode
;
10076 // Disable the option or optgroup if required.
10077 if ( optionWidget
.isDisabled() ) {
10078 $optionNode
.prop( 'disabled', true );
10082 this.optionsDirty
= false;
10088 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
10089 this.dropdownWidget
.focus();
10096 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
10097 this.dropdownWidget
.blur();
10102 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
10103 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
10104 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
10105 * please see the [OOUI documentation on MediaWiki][1].
10107 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10110 * // An example of selected, unselected, and disabled radio inputs
10111 * var radio1 = new OO.ui.RadioInputWidget( {
10115 * var radio2 = new OO.ui.RadioInputWidget( {
10118 * var radio3 = new OO.ui.RadioInputWidget( {
10122 * // Create a fieldset layout with fields for each radio button.
10123 * var fieldset = new OO.ui.FieldsetLayout( {
10124 * label: 'Radio inputs'
10126 * fieldset.addItems( [
10127 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
10128 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
10129 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
10131 * $( document.body ).append( fieldset.$element );
10133 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10136 * @extends OO.ui.InputWidget
10139 * @param {Object} [config] Configuration options
10140 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button
10143 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
10144 // Configuration initialization
10145 config
= config
|| {};
10147 // Parent constructor
10148 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
10152 .addClass( 'oo-ui-radioInputWidget' )
10153 // Required for pretty styling in WikimediaUI theme
10154 .append( $( '<span>' ) );
10155 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
10160 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
10162 /* Static Properties */
10168 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
10170 /* Static Methods */
10175 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10176 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10177 state
.checked
= config
.$input
.prop( 'checked' );
10187 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
10188 return $( '<input>' ).attr( 'type', 'radio' );
10194 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
10195 // RadioInputWidget doesn't track its state.
10199 * Set selection state of this radio button.
10201 * @param {boolean} state `true` for selected
10203 * @return {OO.ui.Widget} The widget, for chaining
10205 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
10206 // RadioInputWidget doesn't track its state.
10207 this.$input
.prop( 'checked', state
);
10208 // The first time that the selection state is set (probably while constructing the widget),
10209 // remember it in defaultSelected. This property can be later used to check whether
10210 // the selection state of the input has been changed since it was created.
10211 if ( this.defaultSelected
=== undefined ) {
10212 this.defaultSelected
= state
;
10213 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
10219 * Check if this radio button is selected.
10221 * @return {boolean} Radio is selected
10223 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
10224 return this.$input
.prop( 'checked' );
10230 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
10231 if ( !this.isDisabled() ) {
10232 this.$input
.trigger( 'click' );
10240 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10241 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10242 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
10243 this.setSelected( state
.checked
);
10248 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be
10249 * used within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with
10250 * the value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
10251 * more information about input widgets.
10253 * This and OO.ui.DropdownInputWidget support similar configuration options.
10256 * // A RadioSelectInputWidget with three options
10257 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
10259 * { data: 'a', label: 'First' },
10260 * { data: 'b', label: 'Second'},
10261 * { data: 'c', label: 'Third' }
10264 * $( document.body ).append( radioSelectInput.$element );
10266 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10269 * @extends OO.ui.InputWidget
10272 * @param {Object} [config] Configuration options
10273 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10275 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
10276 // Configuration initialization
10277 config
= config
|| {};
10279 // Properties (must be done before parent constructor which calls #setDisabled)
10280 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
10281 // Set up the options before parent constructor, which uses them to validate config.value.
10282 // Use this instead of setOptions() because this.$input is not set up yet
10283 this.setOptionsData( config
.options
|| [] );
10285 // Parent constructor
10286 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
10289 this.radioSelectWidget
.connect( this, {
10290 select
: 'onMenuSelect'
10295 .addClass( 'oo-ui-radioSelectInputWidget' )
10296 .append( this.radioSelectWidget
.$element
);
10297 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
10302 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
10304 /* Static Methods */
10309 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10310 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10311 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
10318 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10319 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10320 // Cannot reuse the `<input type=radio>` set
10321 delete config
.$input
;
10331 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
10332 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
10333 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
10334 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
10338 * Handles menu select events.
10341 * @param {OO.ui.RadioOptionWidget} item Selected menu item
10343 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
10344 this.setValue( item
.getData() );
10350 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
10352 value
= this.cleanUpValue( value
);
10353 // Only allow setting values that are actually present in the dropdown
10354 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
10355 this.radioSelectWidget
.findFirstSelectableItem();
10356 this.radioSelectWidget
.selectItem( selected
);
10357 value
= selected
? selected
.getData() : '';
10358 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10365 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10366 this.radioSelectWidget
.setDisabled( state
);
10367 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10372 * Set the options available for this input.
10374 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10376 * @return {OO.ui.Widget} The widget, for chaining
10378 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10379 var value
= this.getValue();
10381 this.setOptionsData( options
);
10383 // Re-set the value to update the visible interface (RadioSelectWidget).
10384 // In case the previous value is no longer an available option, select the first valid one.
10385 this.setValue( value
);
10391 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10393 * This method may be called before the parent constructor, so various properties may not be
10396 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10399 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10402 this.radioSelectWidget
10404 .addItems( options
.map( function ( opt
) {
10405 var optValue
= widget
.cleanUpValue( opt
.data
);
10406 return new OO
.ui
.RadioOptionWidget( {
10408 label
: opt
.label
!== undefined ? opt
.label
: optValue
10416 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10417 this.radioSelectWidget
.focus();
10424 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10425 this.radioSelectWidget
.blur();
10430 * CheckboxMultiselectInputWidget is a
10431 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10432 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10433 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10434 * more information about input widgets.
10437 * // A CheckboxMultiselectInputWidget with three options.
10438 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10440 * { data: 'a', label: 'First' },
10441 * { data: 'b', label: 'Second' },
10442 * { data: 'c', label: 'Third' }
10445 * $( document.body ).append( multiselectInput.$element );
10447 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10450 * @extends OO.ui.InputWidget
10453 * @param {Object} [config] Configuration options
10454 * @cfg {Object[]} [options=[]] Array of menu options in the format
10455 * `{ data: …, label: …, disabled: … }`
10457 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10458 // Configuration initialization
10459 config
= config
|| {};
10461 // Properties (must be done before parent constructor which calls #setDisabled)
10462 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10463 // Must be set before the #setOptionsData call below
10464 this.inputName
= config
.name
;
10465 // Set up the options before parent constructor, which uses them to validate config.value.
10466 // Use this instead of setOptions() because this.$input is not set up yet
10467 this.setOptionsData( config
.options
|| [] );
10469 // Parent constructor
10470 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10473 this.checkboxMultiselectWidget
.connect( this, {
10474 select
: 'onCheckboxesSelect'
10479 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10480 .append( this.checkboxMultiselectWidget
.$element
);
10481 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10482 this.$input
.detach();
10487 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10489 /* Static Methods */
10494 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10495 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState(
10498 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10499 .toArray().map( function ( el
) { return el
.value
; } );
10506 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10507 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10508 // Cannot reuse the `<input type=checkbox>` set
10509 delete config
.$input
;
10519 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10521 return $( '<unused>' );
10525 * Handles CheckboxMultiselectWidget select events.
10529 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10530 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10536 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10537 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10538 .toArray().map( function ( el
) { return el
.value
; } );
10539 if ( this.value
!== value
) {
10540 this.setValue( value
);
10548 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10549 value
= this.cleanUpValue( value
);
10550 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10551 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10552 if ( this.optionsDirty
) {
10553 // We reached this from the constructor or from #setOptions.
10554 // We have to update the <select> element.
10555 this.updateOptionsInterface();
10561 * Clean up incoming value.
10563 * @param {string[]} value Original value
10564 * @return {string[]} Cleaned up value
10566 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10567 var i
, singleValue
,
10569 if ( !Array
.isArray( value
) ) {
10572 for ( i
= 0; i
< value
.length
; i
++ ) {
10573 singleValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10574 .call( this, value
[ i
] );
10575 // Remove options that we don't have here
10576 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10579 cleanValue
.push( singleValue
);
10587 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10588 this.checkboxMultiselectWidget
.setDisabled( state
);
10589 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10594 * Set the options available for this input.
10596 * @param {Object[]} options Array of menu options in the format
10597 * `{ data: …, label: …, disabled: … }`
10599 * @return {OO.ui.Widget} The widget, for chaining
10601 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10602 var value
= this.getValue();
10604 this.setOptionsData( options
);
10606 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10607 // This will also get rid of any stale options that we just removed.
10608 this.setValue( value
);
10614 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10616 * This method may be called before the parent constructor, so various properties may not be
10619 * @param {Object[]} options Array of menu options in the format
10620 * `{ data: …, label: … }`
10623 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10626 this.optionsDirty
= true;
10628 this.checkboxMultiselectWidget
10630 .addItems( options
.map( function ( opt
) {
10631 var optValue
, item
, optDisabled
;
10632 optValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10633 .call( widget
, opt
.data
);
10634 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10635 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10637 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10638 disabled
: optDisabled
10640 // Set the 'name' and 'value' for form submission
10641 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10642 item
.checkbox
.setValue( optValue
);
10648 * Update the user-visible interface to match the internal list of options and value.
10650 * This method must only be called after the parent constructor.
10654 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10655 var defaultValue
= this.defaultValue
;
10657 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10658 // Remember original selection state. This property can be later used to check whether
10659 // the selection state of the input has been changed since it was created.
10660 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10661 item
.checkbox
.defaultSelected
= isDefault
;
10662 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10665 this.optionsDirty
= false;
10671 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10672 this.checkboxMultiselectWidget
.focus();
10677 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10678 * size of the field as well as its presentation. In addition, these widgets can be configured
10679 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an
10680 * optional validation-pattern (used to determine if an input value is valid or not) and an input
10681 * filter, which modifies incoming values rather than validating them.
10682 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10684 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10687 * // A TextInputWidget.
10688 * var textInput = new OO.ui.TextInputWidget( {
10689 * value: 'Text input'
10691 * $( document.body ).append( textInput.$element );
10693 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10696 * @extends OO.ui.InputWidget
10697 * @mixins OO.ui.mixin.IconElement
10698 * @mixins OO.ui.mixin.IndicatorElement
10699 * @mixins OO.ui.mixin.PendingElement
10700 * @mixins OO.ui.mixin.LabelElement
10701 * @mixins OO.ui.mixin.FlaggedElement
10704 * @param {Object} [config] Configuration options
10705 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10706 * 'email', 'url' or 'number'.
10707 * @cfg {string} [placeholder] Placeholder text
10708 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10709 * instruct the browser to focus this widget.
10710 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10711 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10713 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10714 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10715 * many emojis) count as 2 characters each.
10716 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10717 * the value or placeholder text: `'before'` or `'after'`
10718 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator:
10719 * 'required'`. Note that `false` & setting `indicator: 'required' will result in no indicator
10721 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10722 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined`
10723 * means leaving it up to the browser).
10724 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10725 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10726 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10727 * value for it to be considered valid; when Function, a function receiving the value as parameter
10728 * that must return true, or promise resolving to true, for it to be considered valid.
10730 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10731 // Configuration initialization
10732 config
= $.extend( {
10734 labelPosition
: 'after'
10737 // Parent constructor
10738 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10740 // Mixin constructors
10741 OO
.ui
.mixin
.IconElement
.call( this, config
);
10742 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10743 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( { $pending
: this.$input
}, config
) );
10744 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10745 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
10748 this.type
= this.getSaneType( config
);
10749 this.readOnly
= false;
10750 this.required
= false;
10751 this.validate
= null;
10752 this.scrollWidth
= null;
10754 this.setValidation( config
.validate
);
10755 this.setLabelPosition( config
.labelPosition
);
10759 keypress
: this.onKeyPress
.bind( this ),
10760 blur
: this.onBlur
.bind( this ),
10761 focus
: this.onFocus
.bind( this )
10763 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10764 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10765 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10766 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10770 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10771 .append( this.$icon
, this.$indicator
);
10772 this.setReadOnly( !!config
.readOnly
);
10773 this.setRequired( !!config
.required
);
10774 if ( config
.placeholder
!== undefined ) {
10775 this.$input
.attr( 'placeholder', config
.placeholder
);
10777 if ( config
.maxLength
!== undefined ) {
10778 this.$input
.attr( 'maxlength', config
.maxLength
);
10780 if ( config
.autofocus
) {
10781 this.$input
.attr( 'autofocus', 'autofocus' );
10783 if ( config
.autocomplete
=== false ) {
10784 this.$input
.attr( 'autocomplete', 'off' );
10785 // Turning off autocompletion also disables "form caching" when the user navigates to a
10786 // different page and then clicks "Back". Re-enable it when leaving.
10787 // Borrowed from jQuery UI.
10789 beforeunload: function () {
10790 this.$input
.removeAttr( 'autocomplete' );
10792 pageshow: function () {
10793 // Browsers don't seem to actually fire this event on "Back", they instead just
10794 // reload the whole page... it shouldn't hurt, though.
10795 this.$input
.attr( 'autocomplete', 'off' );
10799 if ( config
.spellcheck
!== undefined ) {
10800 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10802 if ( this.label
) {
10803 this.isWaitingToBeAttached
= true;
10804 this.installParentChangeDetector();
10810 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10811 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10812 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10813 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10814 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10815 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
10817 /* Static Properties */
10819 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10827 * An `enter` event is emitted when the user presses Enter key inside the text box.
10835 * Handle icon mouse down events.
10838 * @param {jQuery.Event} e Mouse down event
10839 * @return {undefined|boolean} False to prevent default if event is handled
10841 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10842 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10849 * Handle indicator mouse down events.
10852 * @param {jQuery.Event} e Mouse down event
10853 * @return {undefined|boolean} False to prevent default if event is handled
10855 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10856 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10863 * Handle key press events.
10866 * @param {jQuery.Event} e Key press event
10867 * @fires enter If Enter key is pressed
10869 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10870 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10871 this.emit( 'enter', e
);
10876 * Handle blur events.
10879 * @param {jQuery.Event} e Blur event
10881 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10882 this.setValidityFlag();
10886 * Handle focus events.
10889 * @param {jQuery.Event} e Focus event
10891 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10892 if ( this.isWaitingToBeAttached
) {
10893 // If we've received focus, then we must be attached to the document, and if
10894 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10895 this.onElementAttach();
10897 this.setValidityFlag( true );
10901 * Handle element attach events.
10904 * @param {jQuery.Event} e Element attach event
10906 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10907 this.isWaitingToBeAttached
= false;
10908 // Any previously calculated size is now probably invalid if we reattached elsewhere
10909 this.valCache
= null;
10910 this.positionLabel();
10914 * Handle debounced change events.
10916 * @param {string} value
10919 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10920 this.setValidityFlag();
10924 * Check if the input is {@link #readOnly read-only}.
10926 * @return {boolean}
10928 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10929 return this.readOnly
;
10933 * Set the {@link #readOnly read-only} state of the input.
10935 * @param {boolean} state Make input read-only
10937 * @return {OO.ui.Widget} The widget, for chaining
10939 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10940 this.readOnly
= !!state
;
10941 this.$input
.prop( 'readOnly', this.readOnly
);
10946 * Check if the input is {@link #required required}.
10948 * @return {boolean}
10950 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10951 return this.required
;
10955 * Set the {@link #required required} state of the input.
10957 * @param {boolean} state Make input required
10959 * @return {OO.ui.Widget} The widget, for chaining
10961 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10962 this.required
= !!state
;
10963 if ( this.required
) {
10965 .prop( 'required', true )
10966 .attr( 'aria-required', 'true' );
10967 if ( this.getIndicator() === null ) {
10968 this.setIndicator( 'required' );
10972 .prop( 'required', false )
10973 .removeAttr( 'aria-required' );
10974 if ( this.getIndicator() === 'required' ) {
10975 this.setIndicator( null );
10982 * Support function for making #onElementAttach work across browsers.
10984 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10985 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10987 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10988 * first time that the element gets attached to the documented.
10990 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10991 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10992 MutationObserver
= window
.MutationObserver
||
10993 window
.WebKitMutationObserver
||
10994 window
.MozMutationObserver
,
10997 if ( MutationObserver
) {
10998 // The new way. If only it wasn't so ugly.
11000 if ( this.isElementAttached() ) {
11001 // Widget is attached already, do nothing. This breaks the functionality of this
11002 // function when the widget is detached and reattached. Alas, doing this correctly with
11003 // MutationObserver would require observation of the whole document, which would hurt
11004 // performance of other, more important code.
11008 // Find topmost node in the tree
11009 topmostNode
= this.$element
[ 0 ];
11010 while ( topmostNode
.parentNode
) {
11011 topmostNode
= topmostNode
.parentNode
;
11014 // We have no way to detect the $element being attached somewhere without observing the
11015 // entire DOM with subtree modifications, which would hurt performance. So we cheat: we hook
11016 // to the parent node of $element, and instead detect when $element is removed from it (and
11017 // thus probably attached somewhere else). If there is no parent, we create a "fake" one. If
11018 // it doesn't get attached, we end up back here and create the parent.
11019 mutationObserver
= new MutationObserver( function ( mutations
) {
11020 var i
, j
, removedNodes
;
11021 for ( i
= 0; i
< mutations
.length
; i
++ ) {
11022 removedNodes
= mutations
[ i
].removedNodes
;
11023 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
11024 if ( removedNodes
[ j
] === topmostNode
) {
11025 setTimeout( onRemove
, 0 );
11032 onRemove = function () {
11033 // If the node was attached somewhere else, report it
11034 if ( widget
.isElementAttached() ) {
11035 widget
.onElementAttach();
11037 mutationObserver
.disconnect();
11038 widget
.installParentChangeDetector();
11041 // Create a fake parent and observe it
11042 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
11043 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
11045 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
11046 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
11047 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
11055 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
11056 if ( this.getSaneType( config
) === 'number' ) {
11057 return $( '<input>' )
11058 .attr( 'step', 'any' )
11059 .attr( 'type', 'number' );
11061 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
11066 * Get sanitized value for 'type' for given config.
11068 * @param {Object} config Configuration options
11069 * @return {string|null}
11072 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
11073 var allowedTypes
= [
11080 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
11084 * Focus the input and select a specified range within the text.
11086 * @param {number} from Select from offset
11087 * @param {number} [to] Select to offset, defaults to from
11089 * @return {OO.ui.Widget} The widget, for chaining
11091 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
11092 var isBackwards
, start
, end
,
11093 input
= this.$input
[ 0 ];
11097 isBackwards
= to
< from;
11098 start
= isBackwards
? to
: from;
11099 end
= isBackwards
? from : to
;
11104 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
11106 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
11107 // Rather than expensively check if the input is attached every time, just check
11108 // if it was the cause of an error being thrown. If not, rethrow the error.
11109 if ( this.getElementDocument().body
.contains( input
) ) {
11117 * Get an object describing the current selection range in a directional manner
11119 * @return {Object} Object containing 'from' and 'to' offsets
11121 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
11122 var input
= this.$input
[ 0 ],
11123 start
= input
.selectionStart
,
11124 end
= input
.selectionEnd
,
11125 isBackwards
= input
.selectionDirection
=== 'backward';
11128 from: isBackwards
? end
: start
,
11129 to
: isBackwards
? start
: end
11134 * Get the length of the text input value.
11136 * This could differ from the length of #getValue if the
11137 * value gets filtered
11139 * @return {number} Input length
11141 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
11142 return this.$input
[ 0 ].value
.length
;
11146 * Focus the input and select the entire text.
11149 * @return {OO.ui.Widget} The widget, for chaining
11151 OO
.ui
.TextInputWidget
.prototype.select = function () {
11152 return this.selectRange( 0, this.getInputLength() );
11156 * Focus the input and move the cursor to the start.
11159 * @return {OO.ui.Widget} The widget, for chaining
11161 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
11162 return this.selectRange( 0 );
11166 * Focus the input and move the cursor to the end.
11169 * @return {OO.ui.Widget} The widget, for chaining
11171 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
11172 return this.selectRange( this.getInputLength() );
11176 * Insert new content into the input.
11178 * @param {string} content Content to be inserted
11180 * @return {OO.ui.Widget} The widget, for chaining
11182 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
11184 range
= this.getRange(),
11185 value
= this.getValue();
11187 start
= Math
.min( range
.from, range
.to
);
11188 end
= Math
.max( range
.from, range
.to
);
11190 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
11191 this.selectRange( start
+ content
.length
);
11196 * Insert new content either side of a selection.
11198 * @param {string} pre Content to be inserted before the selection
11199 * @param {string} post Content to be inserted after the selection
11201 * @return {OO.ui.Widget} The widget, for chaining
11203 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
11205 range
= this.getRange(),
11206 offset
= pre
.length
;
11208 start
= Math
.min( range
.from, range
.to
);
11209 end
= Math
.max( range
.from, range
.to
);
11211 this.selectRange( start
).insertContent( pre
);
11212 this.selectRange( offset
+ end
).insertContent( post
);
11214 this.selectRange( offset
+ start
, offset
+ end
);
11219 * Set the validation pattern.
11221 * The validation pattern is either a regular expression, a function, or the symbolic name of a
11222 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11223 * value must contain only numbers).
11225 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
11226 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11228 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
11229 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
11230 this.validate
= validate
;
11232 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
11237 * Sets the 'invalid' flag appropriately.
11239 * @param {boolean} [isValid] Optionally override validation result
11241 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
11243 setFlag = function ( valid
) {
11245 widget
.$input
.attr( 'aria-invalid', 'true' );
11247 widget
.$input
.removeAttr( 'aria-invalid' );
11249 widget
.setFlags( { invalid
: !valid
} );
11252 if ( isValid
!== undefined ) {
11253 setFlag( isValid
);
11255 this.getValidity().then( function () {
11264 * Get the validity of current value.
11266 * This method returns a promise that resolves if the value is valid and rejects if
11267 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
11269 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
11271 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
11274 function rejectOrResolve( valid
) {
11276 return $.Deferred().resolve().promise();
11278 return $.Deferred().reject().promise();
11282 // Check browser validity and reject if it is invalid
11284 this.$input
[ 0 ].checkValidity
!== undefined &&
11285 this.$input
[ 0 ].checkValidity() === false
11287 return rejectOrResolve( false );
11290 // Run our checks if the browser thinks the field is valid
11291 if ( this.validate
instanceof Function
) {
11292 result
= this.validate( this.getValue() );
11293 if ( result
&& typeof result
.promise
=== 'function' ) {
11294 return result
.promise().then( function ( valid
) {
11295 return rejectOrResolve( valid
);
11298 return rejectOrResolve( result
);
11301 return rejectOrResolve( this.getValue().match( this.validate
) );
11306 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11308 * @param {string} labelPosition Label position, 'before' or 'after'
11310 * @return {OO.ui.Widget} The widget, for chaining
11312 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
11313 this.labelPosition
= labelPosition
;
11314 if ( this.label
) {
11315 // If there is no label and we only change the position, #updatePosition is a no-op,
11316 // but it takes really a lot of work to do nothing.
11317 this.updatePosition();
11323 * Update the position of the inline label.
11325 * This method is called by #setLabelPosition, and can also be called on its own if
11326 * something causes the label to be mispositioned.
11329 * @return {OO.ui.Widget} The widget, for chaining
11331 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
11332 var after
= this.labelPosition
=== 'after';
11335 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
11336 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
11338 this.valCache
= null;
11339 this.scrollWidth
= null;
11340 this.positionLabel();
11346 * Position the label by setting the correct padding on the input.
11350 * @return {OO.ui.Widget} The widget, for chaining
11352 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11353 var after
, rtl
, property
, newCss
;
11355 if ( this.isWaitingToBeAttached
) {
11356 // #onElementAttach will be called soon, which calls this method
11361 'padding-right': '',
11365 if ( this.label
) {
11366 this.$element
.append( this.$label
);
11368 this.$label
.detach();
11369 // Clear old values if present
11370 this.$input
.css( newCss
);
11374 after
= this.labelPosition
=== 'after';
11375 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11376 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11378 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11379 // We have to clear the padding on the other side, in case the element direction changed
11380 this.$input
.css( newCss
);
11386 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11387 * {@link OO.ui.mixin.IconElement search icon} by default.
11388 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11390 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11393 * @extends OO.ui.TextInputWidget
11396 * @param {Object} [config] Configuration options
11398 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11399 config
= $.extend( {
11403 // Parent constructor
11404 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11407 this.connect( this, {
11410 this.$indicator
.on( 'click', this.onIndicatorClick
.bind( this ) );
11413 this.updateSearchIndicator();
11414 this.connect( this, {
11415 disable
: 'onDisable'
11421 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11429 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11434 * Handle click events on the indicator
11436 * @param {jQuery.Event} e Click event
11437 * @return {boolean}
11439 OO
.ui
.SearchInputWidget
.prototype.onIndicatorClick = function ( e
) {
11440 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11441 // Clear the text field
11442 this.setValue( '' );
11449 * Update the 'clear' indicator displayed on type: 'search' text
11450 * fields, hiding it when the field is already empty or when it's not
11453 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11454 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11455 this.setIndicator( null );
11457 this.setIndicator( 'clear' );
11462 * Handle change events.
11466 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11467 this.updateSearchIndicator();
11471 * Handle disable events.
11473 * @param {boolean} disabled Element is disabled
11476 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11477 this.updateSearchIndicator();
11483 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11484 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11485 this.updateSearchIndicator();
11490 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11491 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11492 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11493 * {@link OO.ui.mixin.IndicatorElement indicators}.
11494 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11496 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11499 * // A MultilineTextInputWidget.
11500 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11501 * value: 'Text input on multiple lines'
11503 * $( document.body ).append( multilineTextInput.$element );
11505 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11508 * @extends OO.ui.TextInputWidget
11511 * @param {Object} [config] Configuration options
11512 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11513 * specifies minimum number of rows to display.
11514 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11515 * Use the #maxRows config to specify a maximum number of displayed rows.
11516 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11517 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11519 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11520 config
= $.extend( {
11523 // Parent constructor
11524 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11527 this.autosize
= !!config
.autosize
;
11528 this.styleHeight
= null;
11529 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11530 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11532 // Clone for resizing
11533 if ( this.autosize
) {
11534 this.$clone
= this.$input
11536 .removeAttr( 'id' )
11537 .removeAttr( 'name' )
11538 .insertAfter( this.$input
)
11539 .attr( 'aria-hidden', 'true' )
11540 .addClass( 'oo-ui-element-hidden' );
11544 this.connect( this, {
11549 if ( config
.rows
) {
11550 this.$input
.attr( 'rows', config
.rows
);
11552 if ( this.autosize
) {
11553 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11554 this.isWaitingToBeAttached
= true;
11555 this.installParentChangeDetector();
11561 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11563 /* Static Methods */
11568 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11569 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11570 state
.scrollTop
= config
.$input
.scrollTop();
11579 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11580 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11585 * Handle change events.
11589 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11596 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11597 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11604 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11606 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11608 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11609 // Some platforms emit keycode 10 for Control+Enter keypress in a textarea
11612 this.emit( 'enter', e
);
11617 * Automatically adjust the size of the text input.
11619 * This only affects multiline inputs that are {@link #autosize autosized}.
11622 * @return {OO.ui.Widget} The widget, for chaining
11625 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11626 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11627 idealHeight
, newHeight
, scrollWidth
, property
;
11629 if ( this.$input
.val() !== this.valCache
) {
11630 if ( this.autosize
) {
11632 .val( this.$input
.val() )
11633 .attr( 'rows', this.minRows
)
11634 // Set inline height property to 0 to measure scroll height
11635 .css( 'height', 0 );
11637 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11639 this.valCache
= this.$input
.val();
11641 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11643 // Remove inline height property to measure natural heights
11644 this.$clone
.css( 'height', '' );
11645 innerHeight
= this.$clone
.innerHeight();
11646 outerHeight
= this.$clone
.outerHeight();
11648 // Measure max rows height
11650 .attr( 'rows', this.maxRows
)
11651 .css( 'height', 'auto' )
11653 maxInnerHeight
= this.$clone
.innerHeight();
11655 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11656 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11657 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11658 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11660 this.$clone
.addClass( 'oo-ui-element-hidden' );
11662 // Only apply inline height when expansion beyond natural height is needed
11663 // Use the difference between the inner and outer height as a buffer
11664 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11665 if ( newHeight
!== this.styleHeight
) {
11666 this.$input
.css( 'height', newHeight
);
11667 this.styleHeight
= newHeight
;
11668 this.emit( 'resize' );
11671 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11672 if ( scrollWidth
!== this.scrollWidth
) {
11673 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11675 this.$label
.css( { right
: '', left
: '' } );
11676 this.$indicator
.css( { right
: '', left
: '' } );
11678 if ( scrollWidth
) {
11679 this.$indicator
.css( property
, scrollWidth
);
11680 if ( this.labelPosition
=== 'after' ) {
11681 this.$label
.css( property
, scrollWidth
);
11685 this.scrollWidth
= scrollWidth
;
11686 this.positionLabel();
11696 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11697 return $( '<textarea>' );
11701 * Check if the input automatically adjusts its size.
11703 * @return {boolean}
11705 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11706 return !!this.autosize
;
11712 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11713 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11714 if ( state
.scrollTop
!== undefined ) {
11715 this.$input
.scrollTop( state
.scrollTop
);
11720 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11721 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11722 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11724 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11725 * option, that option will appear to be selected.
11726 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11729 * After the user chooses an option, its `data` will be used as a new value for the widget.
11730 * A `label` also can be specified for each option: if given, it will be shown instead of the
11731 * `data` in the dropdown menu.
11733 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11735 * For more information about menus and options, please see the
11736 * [OOUI documentation on MediaWiki][1].
11739 * // A ComboBoxInputWidget.
11740 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11741 * value: 'Option 1',
11743 * { data: 'Option 1' },
11744 * { data: 'Option 2' },
11745 * { data: 'Option 3' }
11748 * $( document.body ).append( comboBox.$element );
11751 * // Example: A ComboBoxInputWidget with additional option labels.
11752 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11753 * value: 'Option 1',
11756 * data: 'Option 1',
11757 * label: 'Option One'
11760 * data: 'Option 2',
11761 * label: 'Option Two'
11764 * data: 'Option 3',
11765 * label: 'Option Three'
11769 * $( document.body ).append( comboBox.$element );
11771 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11774 * @extends OO.ui.TextInputWidget
11777 * @param {Object} [config] Configuration options
11778 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11779 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu
11781 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
11782 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
11783 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
11784 * uses relative positioning.
11785 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11787 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11788 // Configuration initialization
11789 config
= $.extend( {
11790 autocomplete
: false
11793 // ComboBoxInputWidget shouldn't support `multiline`
11794 config
.multiline
= false;
11796 // See InputWidget#reusePreInfuseDOM about `config.$input`
11797 if ( config
.$input
) {
11798 config
.$input
.removeAttr( 'list' );
11801 // Parent constructor
11802 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11805 this.$overlay
= ( config
.$overlay
=== true ?
11806 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11807 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11808 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11809 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11811 invisibleLabel
: true,
11812 disabled
: this.disabled
11814 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11818 $floatableContainer
: this.$element
,
11819 disabled
: this.isDisabled()
11825 this.connect( this, {
11826 change
: 'onInputChange',
11827 enter
: 'onInputEnter'
11829 this.dropdownButton
.connect( this, {
11830 click
: 'onDropdownButtonClick'
11832 this.menu
.connect( this, {
11833 choose
: 'onMenuChoose',
11834 add
: 'onMenuItemsChange',
11835 remove
: 'onMenuItemsChange',
11836 toggle
: 'onMenuToggle'
11840 this.$input
.attr( {
11842 'aria-owns': this.menu
.getElementId(),
11843 'aria-autocomplete': 'list'
11845 this.dropdownButton
.$button
.attr( {
11846 'aria-controls': this.menu
.getElementId()
11848 // Do not override options set via config.menu.items
11849 if ( config
.options
!== undefined ) {
11850 this.setOptions( config
.options
);
11852 this.$field
= $( '<div>' )
11853 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11854 .append( this.$input
, this.dropdownButton
.$element
);
11856 .addClass( 'oo-ui-comboBoxInputWidget' )
11857 .append( this.$field
);
11858 this.$overlay
.append( this.menu
.$element
);
11859 this.onMenuItemsChange();
11864 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11869 * Get the combobox's menu.
11871 * @return {OO.ui.MenuSelectWidget} Menu widget
11873 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11878 * Get the combobox's text input widget.
11880 * @return {OO.ui.TextInputWidget} Text input widget
11882 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11887 * Handle input change events.
11890 * @param {string} value New value
11892 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11893 var match
= this.menu
.findItemFromData( value
);
11895 this.menu
.selectItem( match
);
11896 if ( this.menu
.findHighlightedItem() ) {
11897 this.menu
.highlightItem( match
);
11900 if ( !this.isDisabled() ) {
11901 this.menu
.toggle( true );
11906 * Handle input enter events.
11910 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11911 if ( !this.isDisabled() ) {
11912 this.menu
.toggle( false );
11917 * Handle button click events.
11921 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11922 this.menu
.toggle();
11927 * Handle menu choose events.
11930 * @param {OO.ui.OptionWidget} item Chosen item
11932 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11933 this.setValue( item
.getData() );
11937 * Handle menu item change events.
11941 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11942 var match
= this.menu
.findItemFromData( this.getValue() );
11943 this.menu
.selectItem( match
);
11944 if ( this.menu
.findHighlightedItem() ) {
11945 this.menu
.highlightItem( match
);
11947 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11951 * Handle menu toggle events.
11954 * @param {boolean} isVisible Open state of the menu
11956 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11957 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11961 * Update the disabled state of the controls
11965 * @return {OO.ui.ComboBoxInputWidget} The widget, for chaining
11967 OO
.ui
.ComboBoxInputWidget
.prototype.updateControlsDisabled = function () {
11968 var disabled
= this.isDisabled() || this.isReadOnly();
11969 if ( this.dropdownButton
) {
11970 this.dropdownButton
.setDisabled( disabled
);
11973 this.menu
.setDisabled( disabled
);
11981 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function () {
11983 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.apply( this, arguments
);
11984 this.updateControlsDisabled();
11991 OO
.ui
.ComboBoxInputWidget
.prototype.setReadOnly = function () {
11993 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
11994 this.updateControlsDisabled();
11999 * Set the options available for this input.
12001 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
12003 * @return {OO.ui.Widget} The widget, for chaining
12005 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
12008 .addItems( options
.map( function ( opt
) {
12009 return new OO
.ui
.MenuOptionWidget( {
12011 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
12019 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
12020 * which is a widget that is specified by reference before any optional configuration settings.
12022 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of
12025 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12026 * A left-alignment is used for forms with many fields.
12027 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12028 * A right-alignment is used for long but familiar forms which users tab through,
12029 * verifying the current field with a quick glance at the label.
12030 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12031 * that users fill out from top to bottom.
12032 * - **inline**: The label is placed after the field-widget and aligned to the left.
12033 * An inline-alignment is best used with checkboxes or radio buttons.
12035 * Help text can either be:
12037 * - accessed via a help icon that appears in the upper right corner of the rendered field layout,
12039 * - shown as a subtle explanation below the label.
12041 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`.
12042 * If it is long or not essential, leave `helpInline` to its default, `false`.
12044 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
12046 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12049 * @extends OO.ui.Layout
12050 * @mixins OO.ui.mixin.LabelElement
12051 * @mixins OO.ui.mixin.TitledElement
12054 * @param {OO.ui.Widget} fieldWidget Field widget
12055 * @param {Object} [config] Configuration options
12056 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
12058 * @cfg {Array} [errors] Error messages about the widget, which will be
12059 * displayed below the widget.
12060 * @cfg {Array} [warnings] Warning messages about the widget, which will be
12061 * displayed below the widget.
12062 * @cfg {Array} [successMessages] Success messages on user interactions with the widget,
12063 * which will be displayed below the widget.
12064 * The array may contain strings or OO.ui.HtmlSnippet instances.
12065 * @cfg {Array} [notices] Notices about the widget, which will be displayed
12066 * below the widget.
12067 * The array may contain strings or OO.ui.HtmlSnippet instances.
12068 * These are more visible than `help` messages when `helpInline` is set, and so
12069 * might be good for transient messages.
12070 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
12071 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
12072 * corner of the rendered field; clicking it will display the text in a popup.
12073 * If `helpInline` is `true`, then a subtle description will be shown after the
12075 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
12076 * or shown when the "help" icon is clicked.
12077 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
12079 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12081 * @throws {Error} An error is thrown if no widget is specified
12083 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
12084 // Allow passing positional parameters inside the config object
12085 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12086 config
= fieldWidget
;
12087 fieldWidget
= config
.fieldWidget
;
12090 // Make sure we have required constructor arguments
12091 if ( fieldWidget
=== undefined ) {
12092 throw new Error( 'Widget not found' );
12095 // Configuration initialization
12096 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
12098 // Parent constructor
12099 OO
.ui
.FieldLayout
.parent
.call( this, config
);
12101 // Mixin constructors
12102 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
12103 $label
: $( '<label>' )
12105 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( { $titled
: this.$label
}, config
) );
12108 this.fieldWidget
= fieldWidget
;
12110 this.warnings
= [];
12111 this.successMessages
= [];
12113 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12114 this.$messages
= $( '<div>' );
12115 this.$header
= $( '<span>' );
12116 this.$body
= $( '<div>' );
12118 this.helpInline
= config
.helpInline
;
12121 this.fieldWidget
.connect( this, {
12122 disable
: 'onFieldDisable'
12126 this.$help
= config
.help
?
12127 this.createHelpElement( config
.help
, config
.$overlay
) :
12129 if ( this.fieldWidget
.getInputId() ) {
12130 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
12131 if ( this.helpInline
) {
12132 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
12135 this.$label
.on( 'click', function () {
12136 this.fieldWidget
.simulateLabelClick();
12138 if ( this.helpInline
) {
12139 this.$help
.on( 'click', function () {
12140 this.fieldWidget
.simulateLabelClick();
12145 .addClass( 'oo-ui-fieldLayout' )
12146 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
12147 .append( this.$body
);
12148 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
12149 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
12150 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
12152 .addClass( 'oo-ui-fieldLayout-field' )
12153 .append( this.fieldWidget
.$element
);
12155 this.setErrors( config
.errors
|| [] );
12156 this.setWarnings( config
.warnings
|| [] );
12157 this.setSuccess( config
.successMessages
|| [] );
12158 this.setNotices( config
.notices
|| [] );
12159 this.setAlignment( config
.align
);
12160 // Call this again to take into account the widget's accessKey
12161 this.updateTitle();
12166 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
12167 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
12168 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
12173 * Handle field disable events.
12176 * @param {boolean} value Field is disabled
12178 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
12179 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
12183 * Get the widget contained by the field.
12185 * @return {OO.ui.Widget} Field widget
12187 OO
.ui
.FieldLayout
.prototype.getField = function () {
12188 return this.fieldWidget
;
12192 * Return `true` if the given field widget can be used with `'inline'` alignment (see
12193 * #setAlignment). Return `false` if it can't or if this can't be determined.
12195 * @return {boolean}
12197 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
12198 // This is very simplistic, but should be good enough.
12199 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
12204 * @param {string} kind 'error' or 'notice'
12205 * @param {string|OO.ui.HtmlSnippet} text
12208 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
12209 return new OO
.ui
.MessageWidget( {
12217 * Set the field alignment mode.
12220 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
12222 * @return {OO.ui.BookletLayout} The layout, for chaining
12224 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
12225 if ( value
!== this.align
) {
12226 // Default to 'left'
12227 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
12231 if ( value
=== 'inline' && !this.isFieldInline() ) {
12234 // Reorder elements
12236 if ( this.helpInline
) {
12237 if ( value
=== 'top' ) {
12238 this.$header
.append( this.$label
);
12239 this.$body
.append( this.$header
, this.$field
, this.$help
);
12240 } else if ( value
=== 'inline' ) {
12241 this.$header
.append( this.$label
, this.$help
);
12242 this.$body
.append( this.$field
, this.$header
);
12244 this.$header
.append( this.$label
, this.$help
);
12245 this.$body
.append( this.$header
, this.$field
);
12248 if ( value
=== 'top' ) {
12249 this.$header
.append( this.$help
, this.$label
);
12250 this.$body
.append( this.$header
, this.$field
);
12251 } else if ( value
=== 'inline' ) {
12252 this.$header
.append( this.$help
, this.$label
);
12253 this.$body
.append( this.$field
, this.$header
);
12255 this.$header
.append( this.$label
);
12256 this.$body
.append( this.$header
, this.$help
, this.$field
);
12259 // Set classes. The following classes can be used here:
12260 // * oo-ui-fieldLayout-align-left
12261 // * oo-ui-fieldLayout-align-right
12262 // * oo-ui-fieldLayout-align-top
12263 // * oo-ui-fieldLayout-align-inline
12264 if ( this.align
) {
12265 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
12267 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
12268 this.align
= value
;
12275 * Set the list of error messages.
12277 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
12278 * The array may contain strings or OO.ui.HtmlSnippet instances.
12280 * @return {OO.ui.BookletLayout} The layout, for chaining
12282 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
12283 this.errors
= errors
.slice();
12284 this.updateMessages();
12289 * Set the list of warning messages.
12291 * @param {Array} warnings Warning messages about the widget, which will be displayed below
12293 * The array may contain strings or OO.ui.HtmlSnippet instances.
12295 * @return {OO.ui.BookletLayout} The layout, for chaining
12297 OO
.ui
.FieldLayout
.prototype.setWarnings = function ( warnings
) {
12298 this.warnings
= warnings
.slice();
12299 this.updateMessages();
12304 * Set the list of success messages.
12306 * @param {Array} successMessages Success messages about the widget, which will be displayed below
12308 * The array may contain strings or OO.ui.HtmlSnippet instances.
12310 * @return {OO.ui.BookletLayout} The layout, for chaining
12312 OO
.ui
.FieldLayout
.prototype.setSuccess = function ( successMessages
) {
12313 this.successMessages
= successMessages
.slice();
12314 this.updateMessages();
12319 * Set the list of notice messages.
12321 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
12322 * The array may contain strings or OO.ui.HtmlSnippet instances.
12324 * @return {OO.ui.BookletLayout} The layout, for chaining
12326 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
12327 this.notices
= notices
.slice();
12328 this.updateMessages();
12333 * Update the rendering of error, warning, success and notice messages.
12337 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
12339 this.$messages
.empty();
12342 this.errors
.length
||
12343 this.warnings
.length
||
12344 this.successMessages
.length
||
12345 this.notices
.length
12347 this.$body
.after( this.$messages
);
12349 this.$messages
.remove();
12353 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
12354 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
12356 for ( i
= 0; i
< this.warnings
.length
; i
++ ) {
12357 this.$messages
.append( this.makeMessage( 'warning', this.warnings
[ i
] ) );
12359 for ( i
= 0; i
< this.successMessages
.length
; i
++ ) {
12360 this.$messages
.append( this.makeMessage( 'success', this.successMessages
[ i
] ) );
12362 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
12363 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
12368 * Include information about the widget's accessKey in our title. TitledElement calls this method.
12369 * (This is a bit of a hack.)
12372 * @param {string} title Tooltip label for 'title' attribute
12375 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
12376 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
12377 return this.fieldWidget
.formatTitleWithAccessKey( title
);
12383 * Creates and returns the help element. Also sets the `aria-describedby`
12384 * attribute on the main element of the `fieldWidget`.
12387 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
12388 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
12389 * @return {jQuery} The element that should become `this.$help`.
12391 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
12392 var helpId
, helpWidget
;
12394 if ( this.helpInline
) {
12395 helpWidget
= new OO
.ui
.LabelWidget( {
12397 classes
: [ 'oo-ui-inline-help' ]
12400 helpId
= helpWidget
.getElementId();
12402 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12403 $overlay
: $overlay
,
12407 classes
: [ 'oo-ui-fieldLayout-help' ],
12410 label
: OO
.ui
.msg( 'ooui-field-help' ),
12411 invisibleLabel
: true
12413 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
12414 helpWidget
.getPopup().$body
.html( help
.toString() );
12416 helpWidget
.getPopup().$body
.text( help
);
12419 helpId
= helpWidget
.getPopup().getBodyId();
12422 // Set the 'aria-describedby' attribute on the fieldWidget
12423 // Preference given to an input or a button
12425 this.fieldWidget
.$input
||
12426 this.fieldWidget
.$button
||
12427 this.fieldWidget
.$element
12428 ).attr( 'aria-describedby', helpId
);
12430 return helpWidget
.$element
;
12434 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget,
12435 * a button, and an optional label and/or help text. The field-widget (e.g., a
12436 * {@link OO.ui.TextInputWidget TextInputWidget}), is required and is specified before any optional
12437 * configuration settings.
12439 * Labels can be aligned in one of four ways:
12441 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12442 * A left-alignment is used for forms with many fields.
12443 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12444 * A right-alignment is used for long but familiar forms which users tab through,
12445 * verifying the current field with a quick glance at the label.
12446 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12447 * that users fill out from top to bottom.
12448 * - **inline**: The label is placed after the field-widget and aligned to the left.
12449 * An inline-alignment is best used with checkboxes or radio buttons.
12451 * Help text is accessed via a help icon that appears in the upper right corner of the rendered
12452 * field layout when help text is specified.
12455 * // Example of an ActionFieldLayout
12456 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12457 * new OO.ui.TextInputWidget( {
12458 * placeholder: 'Field widget'
12460 * new OO.ui.ButtonWidget( {
12464 * label: 'An ActionFieldLayout. This label is aligned top',
12466 * help: 'This is help text'
12470 * $( document.body ).append( actionFieldLayout.$element );
12473 * @extends OO.ui.FieldLayout
12476 * @param {OO.ui.Widget} fieldWidget Field widget
12477 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12478 * @param {Object} config
12480 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12481 // Allow passing positional parameters inside the config object
12482 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12483 config
= fieldWidget
;
12484 fieldWidget
= config
.fieldWidget
;
12485 buttonWidget
= config
.buttonWidget
;
12488 // Parent constructor
12489 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12492 this.buttonWidget
= buttonWidget
;
12493 this.$button
= $( '<span>' );
12494 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12497 this.$element
.addClass( 'oo-ui-actionFieldLayout' );
12499 .addClass( 'oo-ui-actionFieldLayout-button' )
12500 .append( this.buttonWidget
.$element
);
12502 .addClass( 'oo-ui-actionFieldLayout-input' )
12503 .append( this.fieldWidget
.$element
);
12504 this.$field
.append( this.$input
, this.$button
);
12509 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12512 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12513 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12514 * configured with a label as well. For more information and examples,
12515 * please see the [OOUI documentation on MediaWiki][1].
12518 * // Example of a fieldset layout
12519 * var input1 = new OO.ui.TextInputWidget( {
12520 * placeholder: 'A text input field'
12523 * var input2 = new OO.ui.TextInputWidget( {
12524 * placeholder: 'A text input field'
12527 * var fieldset = new OO.ui.FieldsetLayout( {
12528 * label: 'Example of a fieldset layout'
12531 * fieldset.addItems( [
12532 * new OO.ui.FieldLayout( input1, {
12533 * label: 'Field One'
12535 * new OO.ui.FieldLayout( input2, {
12536 * label: 'Field Two'
12539 * $( document.body ).append( fieldset.$element );
12541 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12544 * @extends OO.ui.Layout
12545 * @mixins OO.ui.mixin.IconElement
12546 * @mixins OO.ui.mixin.LabelElement
12547 * @mixins OO.ui.mixin.GroupElement
12550 * @param {Object} [config] Configuration options
12551 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset.
12552 * See OO.ui.FieldLayout for more information about fields.
12553 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
12554 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
12555 * corner of the rendered field; clicking it will display the text in a popup.
12556 * If `helpInline` is `true`, then a subtle description will be shown after the
12558 * For feedback messages, you are advised to use `notices`.
12559 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
12560 * or shown when the "help" icon is clicked.
12561 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12562 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12564 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12567 // Configuration initialization
12568 config
= config
|| {};
12570 // Parent constructor
12571 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12573 // Mixin constructors
12574 OO
.ui
.mixin
.IconElement
.call( this, config
);
12575 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12576 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12579 this.$header
= $( '<legend>' );
12583 .addClass( 'oo-ui-fieldsetLayout-header' )
12584 .append( this.$icon
, this.$label
);
12585 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12587 .addClass( 'oo-ui-fieldsetLayout' )
12588 .prepend( this.$header
, this.$group
);
12591 if ( config
.help
) {
12592 if ( config
.helpInline
) {
12593 helpWidget
= new OO
.ui
.LabelWidget( {
12594 label
: config
.help
,
12595 classes
: [ 'oo-ui-inline-help' ]
12597 this.$element
.prepend( this.$header
, helpWidget
.$element
, this.$group
);
12599 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12600 $overlay
: config
.$overlay
,
12604 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12607 label
: OO
.ui
.msg( 'ooui-field-help' ),
12608 invisibleLabel
: true
12610 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12611 helpWidget
.getPopup().$body
.html( config
.help
.toString() );
12613 helpWidget
.getPopup().$body
.text( config
.help
);
12615 this.$header
.append( helpWidget
.$element
);
12618 if ( Array
.isArray( config
.items
) ) {
12619 this.addItems( config
.items
);
12625 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12626 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12627 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12628 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12630 /* Static Properties */
12636 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12639 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use
12640 * browser-based form submission for the fields instead of handling them in JavaScript. Form layouts
12641 * can be configured with an HTML form action, an encoding type, and a method using the #action,
12642 * #enctype, and #method configs, respectively.
12643 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12645 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12646 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12647 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12648 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12649 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12650 * often have simplified APIs to match the capabilities of HTML forms.
12651 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12653 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12654 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12657 * // Example of a form layout that wraps a fieldset layout.
12658 * var input1 = new OO.ui.TextInputWidget( {
12659 * placeholder: 'Username'
12661 * input2 = new OO.ui.TextInputWidget( {
12662 * placeholder: 'Password',
12665 * submit = new OO.ui.ButtonInputWidget( {
12668 * fieldset = new OO.ui.FieldsetLayout( {
12669 * label: 'A form layout'
12672 * fieldset.addItems( [
12673 * new OO.ui.FieldLayout( input1, {
12674 * label: 'Username',
12677 * new OO.ui.FieldLayout( input2, {
12678 * label: 'Password',
12681 * new OO.ui.FieldLayout( submit )
12683 * var form = new OO.ui.FormLayout( {
12684 * items: [ fieldset ],
12685 * action: '/api/formhandler',
12688 * $( document.body ).append( form.$element );
12691 * @extends OO.ui.Layout
12692 * @mixins OO.ui.mixin.GroupElement
12695 * @param {Object} [config] Configuration options
12696 * @cfg {string} [method] HTML form `method` attribute
12697 * @cfg {string} [action] HTML form `action` attribute
12698 * @cfg {string} [enctype] HTML form `enctype` attribute
12699 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12701 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12704 // Configuration initialization
12705 config
= config
|| {};
12707 // Parent constructor
12708 OO
.ui
.FormLayout
.parent
.call( this, config
);
12710 // Mixin constructors
12711 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12714 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12716 // Make sure the action is safe
12717 action
= config
.action
;
12718 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12719 action
= './' + action
;
12724 .addClass( 'oo-ui-formLayout' )
12726 method
: config
.method
,
12728 enctype
: config
.enctype
12730 if ( Array
.isArray( config
.items
) ) {
12731 this.addItems( config
.items
);
12737 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12738 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12743 * A 'submit' event is emitted when the form is submitted.
12748 /* Static Properties */
12754 OO
.ui
.FormLayout
.static.tagName
= 'form';
12759 * Handle form submit events.
12762 * @param {jQuery.Event} e Submit event
12764 * @return {OO.ui.FormLayout} The layout, for chaining
12766 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12767 if ( this.emit( 'submit' ) ) {
12773 * PanelLayouts expand to cover the entire area of their parent. They can be configured with
12774 * scrolling, padding, and a frame, and are often used together with
12775 * {@link OO.ui.StackLayout StackLayouts}.
12778 * // Example of a panel layout
12779 * var panel = new OO.ui.PanelLayout( {
12783 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12785 * $( document.body ).append( panel.$element );
12788 * @extends OO.ui.Layout
12791 * @param {Object} [config] Configuration options
12792 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12793 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12794 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12795 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside
12798 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12799 // Configuration initialization
12800 config
= $.extend( {
12807 // Parent constructor
12808 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12811 this.$element
.addClass( 'oo-ui-panelLayout' );
12812 if ( config
.scrollable
) {
12813 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12815 if ( config
.padded
) {
12816 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12818 if ( config
.expanded
) {
12819 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12821 if ( config
.framed
) {
12822 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12828 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12830 /* Static Methods */
12835 OO
.ui
.PanelLayout
.static.reusePreInfuseDOM = function ( node
, config
) {
12836 config
= OO
.ui
.PanelLayout
.parent
.static.reusePreInfuseDOM( node
, config
);
12837 if ( config
.preserveContent
!== false ) {
12838 config
.$content
= $( node
).contents();
12846 * Focus the panel layout
12848 * The default implementation just focuses the first focusable element in the panel
12850 OO
.ui
.PanelLayout
.prototype.focus = function () {
12851 OO
.ui
.findFocusable( this.$element
).focus();
12855 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12856 * items), with small margins between them. Convenient when you need to put a number of block-level
12857 * widgets on a single line next to each other.
12859 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12862 * // HorizontalLayout with a text input and a label.
12863 * var layout = new OO.ui.HorizontalLayout( {
12865 * new OO.ui.LabelWidget( { label: 'Label' } ),
12866 * new OO.ui.TextInputWidget( { value: 'Text' } )
12869 * $( document.body ).append( layout.$element );
12872 * @extends OO.ui.Layout
12873 * @mixins OO.ui.mixin.GroupElement
12876 * @param {Object} [config] Configuration options
12877 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12879 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12880 // Configuration initialization
12881 config
= config
|| {};
12883 // Parent constructor
12884 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12886 // Mixin constructors
12887 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12890 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12891 if ( Array
.isArray( config
.items
) ) {
12892 this.addItems( config
.items
);
12898 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12899 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12902 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12903 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12904 * (to adjust the value in increments) to allow the user to enter a number.
12907 * // A NumberInputWidget.
12908 * var numberInput = new OO.ui.NumberInputWidget( {
12909 * label: 'NumberInputWidget',
12910 * input: { value: 5 },
12914 * $( document.body ).append( numberInput.$element );
12917 * @extends OO.ui.TextInputWidget
12920 * @param {Object} [config] Configuration options
12921 * @cfg {Object} [minusButton] Configuration options to pass to the
12922 * {@link OO.ui.ButtonWidget decrementing button widget}.
12923 * @cfg {Object} [plusButton] Configuration options to pass to the
12924 * {@link OO.ui.ButtonWidget incrementing button widget}.
12925 * @cfg {number} [min=-Infinity] Minimum allowed value
12926 * @cfg {number} [max=Infinity] Maximum allowed value
12927 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12928 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or Up/Down arrow keys.
12929 * Defaults to `step` if specified, otherwise `1`.
12930 * @cfg {number} [pageStep=10*buttonStep] Delta when using the Page-up/Page-down keys.
12931 * Defaults to 10 times `buttonStep`.
12932 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12934 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12935 var $field
= $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' );
12937 // Configuration initialization
12938 config
= $.extend( {
12944 // For backward compatibility
12945 $.extend( config
, config
.input
);
12948 // Parent constructor
12949 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12953 if ( config
.showButtons
) {
12954 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12956 disabled
: this.isDisabled(),
12958 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12963 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12964 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12966 disabled
: this.isDisabled(),
12968 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12973 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12978 keydown
: this.onKeyDown
.bind( this ),
12979 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12981 if ( config
.showButtons
) {
12982 this.plusButton
.connect( this, {
12983 click
: [ 'onButtonClick', +1 ]
12985 this.minusButton
.connect( this, {
12986 click
: [ 'onButtonClick', -1 ]
12991 $field
.append( this.$input
);
12992 if ( config
.showButtons
) {
12994 .prepend( this.minusButton
.$element
)
12995 .append( this.plusButton
.$element
);
12999 if ( config
.allowInteger
|| config
.isInteger
) {
13000 // Backward compatibility
13003 this.setRange( config
.min
, config
.max
);
13004 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
13005 // Set the validation method after we set step and range
13006 // so that it doesn't immediately call setValidityFlag
13007 this.setValidation( this.validateNumber
.bind( this ) );
13010 .addClass( 'oo-ui-numberInputWidget' )
13011 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
13017 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
13021 // Backward compatibility
13022 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
13023 this.setStep( flag
? 1 : null );
13025 // Backward compatibility
13026 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
13028 // Backward compatibility
13029 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
13030 return this.step
=== 1;
13032 // Backward compatibility
13033 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
13036 * Set the range of allowed values
13038 * @param {number} min Minimum allowed value
13039 * @param {number} max Maximum allowed value
13041 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
13043 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
13047 this.$input
.attr( 'min', this.min
);
13048 this.$input
.attr( 'max', this.max
);
13049 this.setValidityFlag();
13053 * Get the current range
13055 * @return {number[]} Minimum and maximum values
13057 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
13058 return [ this.min
, this.max
];
13062 * Set the stepping deltas
13064 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
13065 * Defaults to `step` if specified, otherwise `1`.
13066 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
13067 * Defaults to 10 times `buttonStep`.
13068 * @param {number|null} [step] If specified, the field only accepts values that are multiples
13071 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
13072 if ( buttonStep
=== undefined ) {
13073 buttonStep
= step
|| 1;
13075 if ( pageStep
=== undefined ) {
13076 pageStep
= 10 * buttonStep
;
13078 if ( step
!== null && step
<= 0 ) {
13079 throw new Error( 'Step value, if given, must be positive' );
13081 if ( buttonStep
<= 0 ) {
13082 throw new Error( 'Button step value must be positive' );
13084 if ( pageStep
<= 0 ) {
13085 throw new Error( 'Page step value must be positive' );
13088 this.buttonStep
= buttonStep
;
13089 this.pageStep
= pageStep
;
13090 this.$input
.attr( 'step', this.step
|| 'any' );
13091 this.setValidityFlag();
13097 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
13098 if ( value
=== '' ) {
13099 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
13100 // so here we make sure an 'empty' value is actually displayed as such.
13101 this.$input
.val( '' );
13103 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
13107 * Get the current stepping values
13109 * @return {number[]} Button step, page step, and validity step
13111 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
13112 return [ this.buttonStep
, this.pageStep
, this.step
];
13116 * Get the current value of the widget as a number
13118 * @return {number} May be NaN, or an invalid number
13120 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
13121 return +this.getValue();
13125 * Adjust the value of the widget
13127 * @param {number} delta Adjustment amount
13129 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
13130 var n
, v
= this.getNumericValue();
13133 if ( isNaN( delta
) || !isFinite( delta
) ) {
13134 throw new Error( 'Delta must be a finite number' );
13137 if ( isNaN( v
) ) {
13141 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
13143 n
= Math
.round( n
/ this.step
) * this.step
;
13148 this.setValue( n
);
13155 * @param {string} value Field value
13156 * @return {boolean}
13158 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
13160 if ( value
=== '' ) {
13161 return !this.isRequired();
13164 if ( isNaN( n
) || !isFinite( n
) ) {
13168 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
13172 if ( n
< this.min
|| n
> this.max
) {
13180 * Handle mouse click events.
13183 * @param {number} dir +1 or -1
13185 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
13186 this.adjustValue( dir
* this.buttonStep
);
13190 * Handle mouse wheel events.
13193 * @param {jQuery.Event} event
13194 * @return {undefined|boolean} False to prevent default if event is handled
13196 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
13199 if ( this.isDisabled() || this.isReadOnly() ) {
13203 if ( this.$input
.is( ':focus' ) ) {
13204 // Standard 'wheel' event
13205 if ( event
.originalEvent
.deltaMode
!== undefined ) {
13206 this.sawWheelEvent
= true;
13208 if ( event
.originalEvent
.deltaY
) {
13209 delta
= -event
.originalEvent
.deltaY
;
13210 } else if ( event
.originalEvent
.deltaX
) {
13211 delta
= event
.originalEvent
.deltaX
;
13214 // Non-standard events
13215 if ( !this.sawWheelEvent
) {
13216 if ( event
.originalEvent
.wheelDeltaX
) {
13217 delta
= -event
.originalEvent
.wheelDeltaX
;
13218 } else if ( event
.originalEvent
.wheelDeltaY
) {
13219 delta
= event
.originalEvent
.wheelDeltaY
;
13220 } else if ( event
.originalEvent
.wheelDelta
) {
13221 delta
= event
.originalEvent
.wheelDelta
;
13222 } else if ( event
.originalEvent
.detail
) {
13223 delta
= -event
.originalEvent
.detail
;
13228 delta
= delta
< 0 ? -1 : 1;
13229 this.adjustValue( delta
* this.buttonStep
);
13237 * Handle key down events.
13240 * @param {jQuery.Event} e Key down event
13241 * @return {undefined|boolean} False to prevent default if event is handled
13243 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
13244 if ( this.isDisabled() || this.isReadOnly() ) {
13248 switch ( e
.which
) {
13249 case OO
.ui
.Keys
.UP
:
13250 this.adjustValue( this.buttonStep
);
13252 case OO
.ui
.Keys
.DOWN
:
13253 this.adjustValue( -this.buttonStep
);
13255 case OO
.ui
.Keys
.PAGEUP
:
13256 this.adjustValue( this.pageStep
);
13258 case OO
.ui
.Keys
.PAGEDOWN
:
13259 this.adjustValue( -this.pageStep
);
13265 * Update the disabled state of the controls
13269 * @return {OO.ui.NumberInputWidget} The widget, for chaining
13271 OO
.ui
.NumberInputWidget
.prototype.updateControlsDisabled = function () {
13272 var disabled
= this.isDisabled() || this.isReadOnly();
13273 if ( this.minusButton
) {
13274 this.minusButton
.setDisabled( disabled
);
13276 if ( this.plusButton
) {
13277 this.plusButton
.setDisabled( disabled
);
13285 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
13287 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13288 this.updateControlsDisabled();
13295 OO
.ui
.NumberInputWidget
.prototype.setReadOnly = function () {
13297 OO
.ui
.NumberInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
13298 this.updateControlsDisabled();
13303 * SelectFileInputWidgets allow for selecting files, using <input type="file">. These
13304 * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
13305 * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
13306 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
13308 * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename.
13311 * // A file select input widget.
13312 * var selectFile = new OO.ui.SelectFileInputWidget();
13313 * $( document.body ).append( selectFile.$element );
13315 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
13318 * @extends OO.ui.InputWidget
13321 * @param {Object} [config] Configuration options
13322 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
13323 * @cfg {boolean} [multiple=false] Allow multiple files to be selected.
13324 * @cfg {string} [placeholder] Text to display when no file is selected.
13325 * @cfg {Object} [button] Config to pass to select file button.
13326 * @cfg {string} [icon] Icon to show next to file info
13328 OO
.ui
.SelectFileInputWidget
= function OoUiSelectFileInputWidget( config
) {
13331 config
= config
|| {};
13333 // Construct buttons before parent method is called (calling setDisabled)
13334 this.selectButton
= new OO
.ui
.ButtonWidget( $.extend( {
13335 $element
: $( '<label>' ),
13336 classes
: [ 'oo-ui-selectFileInputWidget-selectButton' ],
13337 label
: OO
.ui
.msg( 'ooui-selectfile-button-select' )
13338 }, config
.button
) );
13340 // Configuration initialization
13341 config
= $.extend( {
13343 placeholder
: OO
.ui
.msg( 'ooui-selectfile-placeholder' ),
13344 $tabIndexed
: this.selectButton
.$tabIndexed
13347 this.info
= new OO
.ui
.SearchInputWidget( {
13348 classes
: [ 'oo-ui-selectFileInputWidget-info' ],
13349 placeholder
: config
.placeholder
,
13350 // Pass an empty collection so that .focus() always does nothing
13351 $tabIndexed
: $( [] )
13352 } ).setIcon( config
.icon
);
13353 // Set tabindex manually on $input as $tabIndexed has been overridden
13354 this.info
.$input
.attr( 'tabindex', -1 );
13356 // Parent constructor
13357 OO
.ui
.SelectFileInputWidget
.parent
.call( this, config
);
13360 this.currentFiles
= this.filterFiles( this.$input
[ 0 ].files
|| [] );
13361 if ( Array
.isArray( config
.accept
) ) {
13362 this.accept
= config
.accept
;
13364 this.accept
= null;
13366 this.multiple
= !!config
.multiple
;
13369 this.info
.connect( this, { change
: 'onInfoChange' } );
13370 this.selectButton
.$button
.on( {
13371 keypress
: this.onKeyPress
.bind( this )
13374 change
: this.onFileSelected
.bind( this ),
13376 // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
13377 // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
13378 // Since this messes with our custom styling (the file input has large dimensions and this
13379 // causes the label to scroll out of view), scroll the button back to top. (T192131)
13380 focus: function () {
13381 widget
.$input
.parent().prop( 'scrollTop', 0 );
13384 this.connect( this, { change
: 'updateUI' } );
13386 this.fieldLayout
= new OO
.ui
.ActionFieldLayout( this.info
, this.selectButton
, { align
: 'top' } );
13391 // this.selectButton is tabindexed
13393 // Infused input may have previously by
13394 // TabIndexed, so remove aria-disabled attr.
13395 'aria-disabled': null
13398 if ( this.accept
) {
13399 this.$input
.attr( 'accept', this.accept
.join( ', ' ) );
13401 if ( this.multiple
) {
13402 this.$input
.attr( 'multiple', '' );
13404 this.selectButton
.$button
.append( this.$input
);
13407 .addClass( 'oo-ui-selectFileInputWidget' )
13408 .append( this.fieldLayout
.$element
);
13415 OO
.inheritClass( OO
.ui
.SelectFileInputWidget
, OO
.ui
.InputWidget
);
13417 /* Static properties */
13419 // Set empty title so that browser default tooltips like "No file chosen" don't appear.
13420 // On SelectFileWidget this tooltip will often be incorrect, so create a consistent
13421 // experience on SelectFileInputWidget.
13422 OO
.ui
.SelectFileInputWidget
.static.title
= '';
13427 * Get the filename of the currently selected file.
13429 * @return {string} Filename
13431 OO
.ui
.SelectFileInputWidget
.prototype.getFilename = function () {
13432 if ( this.currentFiles
.length
) {
13433 return this.currentFiles
.map( function ( file
) {
13437 // Try to strip leading fakepath.
13438 return this.getValue().split( '\\' ).pop();
13445 OO
.ui
.SelectFileInputWidget
.prototype.setValue = function ( value
) {
13446 if ( value
=== undefined ) {
13447 // Called during init, don't replace value if just infusing.
13451 // We need to update this.value, but without trying to modify
13452 // the DOM value, which would throw an exception.
13453 if ( this.value
!== value
) {
13454 this.value
= value
;
13455 this.emit( 'change', this.value
);
13458 this.currentFiles
= [];
13460 OO
.ui
.SelectFileInputWidget
.super.prototype.setValue
.call( this, '' );
13465 * Handle file selection from the input.
13468 * @param {jQuery.Event} e
13470 OO
.ui
.SelectFileInputWidget
.prototype.onFileSelected = function ( e
) {
13471 this.currentFiles
= this.filterFiles( e
.target
.files
|| [] );
13475 * Update the user interface when a file is selected or unselected.
13479 OO
.ui
.SelectFileInputWidget
.prototype.updateUI = function () {
13480 this.info
.setValue( this.getFilename() );
13484 * Determine if we should accept this file.
13487 * @param {FileList|File[]} files Files to filter
13488 * @return {File[]} Filter files
13490 OO
.ui
.SelectFileInputWidget
.prototype.filterFiles = function ( files
) {
13491 var accept
= this.accept
;
13493 function mimeAllowed( file
) {
13495 mimeType
= file
.type
;
13497 if ( !accept
|| !mimeType
) {
13501 for ( i
= 0; i
< accept
.length
; i
++ ) {
13502 mimeTest
= accept
[ i
];
13503 if ( mimeTest
=== mimeType
) {
13505 } else if ( mimeTest
.substr( -2 ) === '/*' ) {
13506 mimeTest
= mimeTest
.substr( 0, mimeTest
.length
- 1 );
13507 if ( mimeType
.substr( 0, mimeTest
.length
) === mimeTest
) {
13515 return Array
.prototype.filter
.call( files
, mimeAllowed
);
13519 * Handle info input change events
13521 * The info widget can only be changed by the user
13522 * with the clear button.
13525 * @param {string} value
13527 OO
.ui
.SelectFileInputWidget
.prototype.onInfoChange = function ( value
) {
13528 if ( value
=== '' ) {
13529 this.setValue( null );
13534 * Handle key press events.
13537 * @param {jQuery.Event} e Key press event
13538 * @return {undefined|boolean} False to prevent default if event is handled
13540 OO
.ui
.SelectFileInputWidget
.prototype.onKeyPress = function ( e
) {
13541 if ( !this.isDisabled() && this.$input
&&
13542 ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
13544 // Emit a click to open the file selector.
13545 this.$input
.trigger( 'click' );
13546 // Taking focus from the selectButton means keyUp isn't fired, so fire it manually.
13547 this.selectButton
.onDocumentKeyUp( e
);
13555 OO
.ui
.SelectFileInputWidget
.prototype.setDisabled = function ( disabled
) {
13557 OO
.ui
.SelectFileInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13559 this.selectButton
.setDisabled( disabled
);
13560 this.info
.setDisabled( disabled
);
13567 //# sourceMappingURL=oojs-ui-core.js.map.json