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-06-27T03:27:26Z
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
: $win
.scrollLeft()
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
: $el
.scrollLeft()
1074 right
: $el
.innerWidth() - el
.clientWidth
,
1075 bottom
: $el
.innerHeight() - el
.clientHeight
1077 rect
: el
.getBoundingClientRect()
1083 * Get the number of pixels that an element's content is scrolled to the left.
1085 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1086 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1088 * This function smooths out browser inconsistencies (nicely described in the README at
1089 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1090 * with Firefox's 'scrollLeft', which seems the sanest.
1094 * @param {HTMLElement|Window} el Element to measure
1095 * @return {number} Scroll position from the left.
1096 * If the element's direction is LTR, this is a positive number between `0` (initial scroll
1097 * position) and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1098 * If the element's direction is RTL, this is a negative number between `0` (initial scroll
1099 * position) and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1101 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1102 var rtlScrollType
= null;
1105 var $definer
= $( '<div>' ).attr( {
1107 style
: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
1109 definer
= $definer
[ 0 ];
1111 $definer
.appendTo( 'body' );
1112 if ( definer
.scrollLeft
> 0 ) {
1114 rtlScrollType
= 'default';
1116 definer
.scrollLeft
= 1;
1117 if ( definer
.scrollLeft
=== 0 ) {
1118 // Firefox, old Opera
1119 rtlScrollType
= 'negative';
1121 // Internet Explorer, Edge
1122 rtlScrollType
= 'reverse';
1128 return function getScrollLeft( el
) {
1129 var isRoot
= el
.window
=== el
||
1130 el
=== el
.ownerDocument
.body
||
1131 el
=== el
.ownerDocument
.documentElement
,
1132 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1133 // All browsers use the correct scroll type ('negative') on the root, so don't
1134 // do any fixups when looking at the root element
1135 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1137 if ( direction
=== 'rtl' ) {
1138 if ( rtlScrollType
=== null ) {
1141 if ( rtlScrollType
=== 'reverse' ) {
1142 scrollLeft
= -scrollLeft
;
1143 } else if ( rtlScrollType
=== 'default' ) {
1144 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1153 * Get the root scrollable element of given element's document.
1155 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1156 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1157 * lets us use 'body' or 'documentElement' based on what is working.
1159 * https://code.google.com/p/chromium/issues/detail?id=303131
1162 * @param {HTMLElement} el Element to find root scrollable parent for
1163 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1164 * depending on browser
1166 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1167 var scrollTop
, body
;
1169 if ( OO
.ui
.scrollableElement
=== undefined ) {
1170 body
= el
.ownerDocument
.body
;
1171 scrollTop
= body
.scrollTop
;
1174 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1175 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1176 if ( Math
.round( body
.scrollTop
) === 1 ) {
1177 body
.scrollTop
= scrollTop
;
1178 OO
.ui
.scrollableElement
= 'body';
1180 OO
.ui
.scrollableElement
= 'documentElement';
1184 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1188 * Get closest scrollable container.
1190 * Traverses up until either a scrollable element or the root is reached, in which case the root
1191 * scrollable element will be returned (see #getRootScrollableElement).
1194 * @param {HTMLElement} el Element to find scrollable container for
1195 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1196 * @return {HTMLElement} Closest scrollable container
1198 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1200 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1201 // 'overflow-y' have different values, so we need to check the separate properties.
1202 props
= [ 'overflow-x', 'overflow-y' ],
1203 $parent
= $( el
).parent();
1205 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1206 props
= [ 'overflow-' + dimension
];
1209 // Special case for the document root (which doesn't really have any scrollable container,
1210 // since it is the ultimate scrollable container, but this is probably saner than null or
1212 if ( $( el
).is( 'html, body' ) ) {
1213 return this.getRootScrollableElement( el
);
1216 while ( $parent
.length
) {
1217 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1218 return $parent
[ 0 ];
1222 val
= $parent
.css( props
[ i
] );
1223 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will
1224 // never be scrolled in that direction, but they can actually be scrolled
1225 // programatically. The user can unintentionally perform a scroll in such case even if
1226 // the application doesn't scroll programatically, e.g. when jumping to an anchor, or
1227 // when using built-in find functionality.
1228 // This could cause funny issues...
1229 if ( val
=== 'auto' || val
=== 'scroll' ) {
1230 return $parent
[ 0 ];
1233 $parent
= $parent
.parent();
1235 // The element is unattached... return something mostly sane
1236 return this.getRootScrollableElement( el
);
1240 * Scroll element into view.
1243 * @param {HTMLElement|Object} elOrPosition Element to scroll into view
1244 * @param {Object} [config] Configuration options
1245 * @param {string} [config.animate=true] Animate to the new scroll offset.
1246 * @param {string} [config.duration='fast'] jQuery animation duration value
1247 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1248 * to scroll in both directions
1249 * @param {Object} [config.padding] Additional padding on the container to scroll past.
1250 * Object containing any of 'top', 'bottom', 'left', or 'right' as numbers.
1251 * @param {Object} [config.scrollContainer] Scroll container. Defaults to
1252 * getClosestScrollableContainer of the element.
1253 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1255 OO
.ui
.Element
.static.scrollIntoView = function ( elOrPosition
, config
) {
1256 var position
, animations
, container
, $container
, elementPosition
, containerDimensions
,
1257 $window
, padding
, animate
, method
,
1258 deferred
= $.Deferred();
1260 // Configuration initialization
1261 config
= config
|| {};
1263 padding
= $.extend( {
1268 }, config
.padding
);
1270 animate
= config
.animate
!== false;
1273 elementPosition
= elOrPosition
instanceof HTMLElement
?
1274 this.getDimensions( elOrPosition
).rect
:
1276 container
= config
.scrollContainer
|| (
1277 elOrPosition
instanceof HTMLElement
?
1278 this.getClosestScrollableContainer( elOrPosition
, config
.direction
) :
1279 // No scrollContainer or element
1280 this.getClosestScrollableContainer( document
.body
)
1282 $container
= $( container
);
1283 containerDimensions
= this.getDimensions( container
);
1284 $window
= $( this.getWindow( container
) );
1286 // Compute the element's position relative to the container
1287 if ( $container
.is( 'html, body' ) ) {
1288 // If the scrollable container is the root, this is easy
1290 top
: elementPosition
.top
,
1291 bottom
: $window
.innerHeight() - elementPosition
.bottom
,
1292 left
: elementPosition
.left
,
1293 right
: $window
.innerWidth() - elementPosition
.right
1296 // Otherwise, we have to subtract el's coordinates from container's coordinates
1298 top
: elementPosition
.top
-
1299 ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1300 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
-
1301 containerDimensions
.scrollbar
.bottom
- elementPosition
.bottom
,
1302 left
: elementPosition
.left
-
1303 ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1304 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
-
1305 containerDimensions
.scrollbar
.right
- elementPosition
.right
1309 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1310 if ( position
.top
< padding
.top
) {
1311 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
- padding
.top
;
1312 } else if ( position
.bottom
< padding
.bottom
) {
1313 animations
.scrollTop
= containerDimensions
.scroll
.top
+
1314 // Scroll the bottom into view, but not at the expense
1315 // of scrolling the top out of view
1316 Math
.min( position
.top
- padding
.top
, -position
.bottom
+ padding
.bottom
);
1319 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1320 if ( position
.left
< padding
.left
) {
1321 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
- padding
.left
;
1322 } else if ( position
.right
< padding
.right
) {
1323 animations
.scrollLeft
= containerDimensions
.scroll
.left
+
1324 // Scroll the right into view, but not at the expense
1325 // of scrolling the left out of view
1326 Math
.min( position
.left
- padding
.left
, -position
.right
+ padding
.right
);
1329 if ( !$.isEmptyObject( animations
) ) {
1331 // eslint-disable-next-line no-jquery/no-animate
1332 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1333 $container
.queue( function ( next
) {
1338 $container
.stop( true );
1339 for ( method
in animations
) {
1340 $container
[ method
]( animations
[ method
] );
1347 return deferred
.promise();
1351 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1352 * and reserve space for them, because it probably doesn't.
1354 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1355 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1356 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a
1357 * reflow, and then reattach (or show) them back.
1360 * @param {HTMLElement} el Element to reconsider the scrollbars on
1362 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1363 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1364 // Save scroll position
1365 scrollLeft
= el
.scrollLeft
;
1366 scrollTop
= el
.scrollTop
;
1367 // Detach all children
1368 while ( el
.firstChild
) {
1369 nodes
.push( el
.firstChild
);
1370 el
.removeChild( el
.firstChild
);
1373 // eslint-disable-next-line no-void
1374 void el
.offsetHeight
;
1375 // Reattach all children
1376 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1377 el
.appendChild( nodes
[ i
] );
1379 // Restore scroll position (no-op if scrollbars disappeared)
1380 el
.scrollLeft
= scrollLeft
;
1381 el
.scrollTop
= scrollTop
;
1387 * Toggle visibility of an element.
1389 * @param {boolean} [show] Make element visible, omit to toggle visibility
1392 * @return {OO.ui.Element} The element, for chaining
1394 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1395 show
= show
=== undefined ? !this.visible
: !!show
;
1397 if ( show
!== this.isVisible() ) {
1398 this.visible
= show
;
1399 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1400 this.emit( 'toggle', show
);
1407 * Check if element is visible.
1409 * @return {boolean} element is visible
1411 OO
.ui
.Element
.prototype.isVisible = function () {
1412 return this.visible
;
1418 * @return {Mixed} Element data
1420 OO
.ui
.Element
.prototype.getData = function () {
1427 * @param {Mixed} data Element data
1429 * @return {OO.ui.Element} The element, for chaining
1431 OO
.ui
.Element
.prototype.setData = function ( data
) {
1437 * Set the element has an 'id' attribute.
1439 * @param {string} id
1441 * @return {OO.ui.Element} The element, for chaining
1443 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1444 this.elementId
= id
;
1445 this.$element
.attr( 'id', id
);
1450 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1451 * and return its value.
1455 OO
.ui
.Element
.prototype.getElementId = function () {
1456 if ( this.elementId
=== null ) {
1457 this.setElementId( OO
.ui
.generateElementId() );
1459 return this.elementId
;
1463 * Check if element supports one or more methods.
1465 * @param {string|string[]} methods Method or list of methods to check
1466 * @return {boolean} All methods are supported
1468 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1472 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1473 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1474 if ( typeof this[ methods
[ i
] ] === 'function' ) {
1479 return methods
.length
=== support
;
1483 * Update the theme-provided classes.
1485 * @localdoc This is called in element mixins and widget classes any time state changes.
1486 * Updating is debounced, minimizing overhead of changing multiple attributes and
1487 * guaranteeing that theme updates do not occur within an element's constructor
1489 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1490 OO
.ui
.theme
.queueUpdateElementClasses( this );
1494 * Get the HTML tag name.
1496 * Override this method to base the result on instance information.
1498 * @return {string} HTML tag name
1500 OO
.ui
.Element
.prototype.getTagName = function () {
1501 return this.constructor.static.tagName
;
1505 * Check if the element is attached to the DOM
1507 * @return {boolean} The element is attached to the DOM
1509 OO
.ui
.Element
.prototype.isElementAttached = function () {
1510 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1514 * Get the DOM document.
1516 * @return {HTMLDocument} Document object
1518 OO
.ui
.Element
.prototype.getElementDocument = function () {
1519 // Don't cache this in other ways either because subclasses could can change this.$element
1520 return OO
.ui
.Element
.static.getDocument( this.$element
);
1524 * Get the DOM window.
1526 * @return {Window} Window object
1528 OO
.ui
.Element
.prototype.getElementWindow = function () {
1529 return OO
.ui
.Element
.static.getWindow( this.$element
);
1533 * Get closest scrollable container.
1535 * @return {HTMLElement} Closest scrollable container
1537 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1538 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1542 * Get group element is in.
1544 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1546 OO
.ui
.Element
.prototype.getElementGroup = function () {
1547 return this.elementGroup
;
1551 * Set group element is in.
1553 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1555 * @return {OO.ui.Element} The element, for chaining
1557 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1558 this.elementGroup
= group
;
1563 * Scroll element into view.
1565 * @param {Object} [config] Configuration options
1566 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1568 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1570 !this.isElementAttached() ||
1571 !this.isVisible() ||
1572 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1574 return $.Deferred().resolve();
1576 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1580 * Restore the pre-infusion dynamic state for this widget.
1582 * This method is called after #$element has been inserted into DOM. The parameter is the return
1583 * value of #gatherPreInfuseState.
1586 * @param {Object} state
1588 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1592 * Wraps an HTML snippet for use with configuration values which default
1593 * to strings. This bypasses the default html-escaping done to string
1599 * @param {string} [content] HTML content
1601 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1603 this.content
= content
;
1608 OO
.initClass( OO
.ui
.HtmlSnippet
);
1615 * @return {string} Unchanged HTML snippet.
1617 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1618 return this.content
;
1622 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in
1623 * a way that is centrally controlled and can be updated dynamically. Layouts can be, and usually
1625 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout},
1626 * {@link OO.ui.FormLayout FormLayout}, {@link OO.ui.PanelLayout PanelLayout},
1627 * {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1628 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout}
1629 * for more information and examples.
1633 * @extends OO.ui.Element
1634 * @mixins OO.EventEmitter
1637 * @param {Object} [config] Configuration options
1639 OO
.ui
.Layout
= function OoUiLayout( config
) {
1640 // Configuration initialization
1641 config
= config
|| {};
1643 // Parent constructor
1644 OO
.ui
.Layout
.parent
.call( this, config
);
1646 // Mixin constructors
1647 OO
.EventEmitter
.call( this );
1650 this.$element
.addClass( 'oo-ui-layout' );
1655 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1656 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1661 * Reset scroll offsets
1664 * @return {OO.ui.Layout} The layout, for chaining
1666 OO
.ui
.Layout
.prototype.resetScroll = function () {
1667 this.$element
[ 0 ].scrollTop
= 0;
1668 // TODO: Reset scrollLeft in an RTL-aware manner, see OO.ui.Element.static.getScrollLeft.
1674 * Widgets are compositions of one or more OOUI elements that users can both view
1675 * and interact with. All widgets can be configured and modified via a standard API,
1676 * and their state can change dynamically according to a model.
1680 * @extends OO.ui.Element
1681 * @mixins OO.EventEmitter
1684 * @param {Object} [config] Configuration options
1685 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1686 * appearance reflects this state.
1688 OO
.ui
.Widget
= function OoUiWidget( config
) {
1689 // Initialize config
1690 config
= $.extend( { disabled
: false }, config
);
1692 // Parent constructor
1693 OO
.ui
.Widget
.parent
.call( this, config
);
1695 // Mixin constructors
1696 OO
.EventEmitter
.call( this );
1699 this.disabled
= null;
1700 this.wasDisabled
= null;
1703 this.$element
.addClass( 'oo-ui-widget' );
1704 this.setDisabled( !!config
.disabled
);
1709 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1710 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1717 * A 'disable' event is emitted when the disabled state of the widget changes
1718 * (i.e. on disable **and** enable).
1720 * @param {boolean} disabled Widget is disabled
1726 * A 'toggle' event is emitted when the visibility of the widget changes.
1728 * @param {boolean} visible Widget is visible
1734 * Check if the widget is disabled.
1736 * @return {boolean} Widget is disabled
1738 OO
.ui
.Widget
.prototype.isDisabled = function () {
1739 return this.disabled
;
1743 * Set the 'disabled' state of the widget.
1745 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1747 * @param {boolean} disabled Disable widget
1749 * @return {OO.ui.Widget} The widget, for chaining
1751 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1754 this.disabled
= !!disabled
;
1755 isDisabled
= this.isDisabled();
1756 if ( isDisabled
!== this.wasDisabled
) {
1757 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1758 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1759 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1760 this.emit( 'disable', isDisabled
);
1761 this.updateThemeClasses();
1763 this.wasDisabled
= isDisabled
;
1769 * Update the disabled state, in case of changes in parent widget.
1772 * @return {OO.ui.Widget} The widget, for chaining
1774 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1775 this.setDisabled( this.disabled
);
1780 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1783 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1786 * @return {string|null} The ID of the labelable element
1788 OO
.ui
.Widget
.prototype.getInputId = function () {
1793 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1794 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1795 * override this method to provide intuitive, accessible behavior.
1797 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1798 * Individual widgets may override it too.
1800 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1803 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1814 OO
.ui
.Theme
= function OoUiTheme() {
1815 this.elementClassesQueue
= [];
1816 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1821 OO
.initClass( OO
.ui
.Theme
);
1826 * Get a list of classes to be applied to a widget.
1828 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1829 * otherwise state transitions will not work properly.
1831 * @param {OO.ui.Element} element Element for which to get classes
1832 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1834 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1835 return { on
: [], off
: [] };
1839 * Update CSS classes provided by the theme.
1841 * For elements with theme logic hooks, this should be called any time there's a state change.
1843 * @param {OO.ui.Element} element Element for which to update classes
1845 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1846 var $elements
= $( [] ),
1847 classes
= this.getElementClasses( element
);
1849 if ( element
.$icon
) {
1850 $elements
= $elements
.add( element
.$icon
);
1852 if ( element
.$indicator
) {
1853 $elements
= $elements
.add( element
.$indicator
);
1857 .removeClass( classes
.off
)
1858 .addClass( classes
.on
);
1864 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1866 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1867 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1870 this.elementClassesQueue
= [];
1874 * Queue #updateElementClasses to be called for this element.
1876 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1877 * to make them synchronous.
1879 * @param {OO.ui.Element} element Element for which to update classes
1881 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1882 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1883 // the most common case (this method is often called repeatedly for the same element).
1884 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1887 this.elementClassesQueue
.push( element
);
1888 this.debouncedUpdateQueuedElementClasses();
1892 * Get the transition duration in milliseconds for dialogs opening/closing
1894 * The dialog should be fully rendered this many milliseconds after the
1895 * ready process has executed.
1897 * @return {number} Transition duration in milliseconds
1899 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1904 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1905 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1906 * order in which users will navigate through the focusable elements via the Tab key.
1909 * // TabIndexedElement is mixed into the ButtonWidget class
1910 * // to provide a tabIndex property.
1911 * var button1 = new OO.ui.ButtonWidget( {
1915 * button2 = new OO.ui.ButtonWidget( {
1919 * button3 = new OO.ui.ButtonWidget( {
1923 * button4 = new OO.ui.ButtonWidget( {
1927 * $( document.body ).append(
1938 * @param {Object} [config] Configuration options
1939 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1940 * the functionality is applied to the element created by the class ($element). If a different
1941 * element is specified, the tabindex functionality will be applied to it instead.
1942 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the
1943 * tab-navigation order (e.g., 1 for the first focusable element). Use 0 to use the default
1944 * navigation order; use -1 to remove the element from the tab-navigation flow.
1946 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1947 // Configuration initialization
1948 config
= $.extend( { tabIndex
: 0 }, config
);
1951 this.$tabIndexed
= null;
1952 this.tabIndex
= null;
1955 this.connect( this, {
1956 disable
: 'onTabIndexedElementDisable'
1960 this.setTabIndex( config
.tabIndex
);
1961 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1966 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1971 * Set the element that should use the tabindex functionality.
1973 * This method is used to retarget a tabindex mixin so that its functionality applies
1974 * to the specified element. If an element is currently using the functionality, the mixin’s
1975 * effect on that element is removed before the new element is set up.
1977 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1979 * @return {OO.ui.Element} The element, for chaining
1981 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1982 var tabIndex
= this.tabIndex
;
1983 // Remove attributes from old $tabIndexed
1984 this.setTabIndex( null );
1985 // Force update of new $tabIndexed
1986 this.$tabIndexed
= $tabIndexed
;
1987 this.tabIndex
= tabIndex
;
1988 return this.updateTabIndex();
1992 * Set the value of the tabindex.
1994 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1996 * @return {OO.ui.Element} The element, for chaining
1998 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1999 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
2001 if ( this.tabIndex
!== tabIndex
) {
2002 this.tabIndex
= tabIndex
;
2003 this.updateTabIndex();
2010 * Update the `tabindex` attribute, in case of changes to tab index or
2015 * @return {OO.ui.Element} The element, for chaining
2017 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
2018 if ( this.$tabIndexed
) {
2019 if ( this.tabIndex
!== null ) {
2020 // Do not index over disabled elements
2021 this.$tabIndexed
.attr( {
2022 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
2023 // Support: ChromeVox and NVDA
2024 // These do not seem to inherit aria-disabled from parent elements
2025 'aria-disabled': this.isDisabled().toString()
2028 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
2035 * Handle disable events.
2038 * @param {boolean} disabled Element is disabled
2040 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
2041 this.updateTabIndex();
2045 * Get the value of the tabindex.
2047 * @return {number|null} Tabindex value
2049 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2050 return this.tabIndex
;
2054 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2056 * If the element already has an ID then that is returned, otherwise unique ID is
2057 * generated, set on the element, and returned.
2059 * @return {string|null} The ID of the focusable element
2061 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2064 if ( !this.$tabIndexed
) {
2067 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2071 id
= this.$tabIndexed
.attr( 'id' );
2072 if ( id
=== undefined ) {
2073 id
= OO
.ui
.generateElementId();
2074 this.$tabIndexed
.attr( 'id', id
);
2081 * Whether the node is 'labelable' according to the HTML spec
2082 * (i.e., whether it can be interacted with through a `<label for="…">`).
2083 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2086 * @param {jQuery} $node
2089 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2091 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2092 tagName
= ( $node
.prop( 'tagName' ) || '' ).toLowerCase();
2094 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2097 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2104 * Focus this element.
2107 * @return {OO.ui.Element} The element, for chaining
2109 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2110 if ( !this.isDisabled() ) {
2111 this.$tabIndexed
.trigger( 'focus' );
2117 * Blur this element.
2120 * @return {OO.ui.Element} The element, for chaining
2122 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2123 this.$tabIndexed
.trigger( 'blur' );
2128 * @inheritdoc OO.ui.Widget
2130 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2135 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2136 * interface element that can be configured with access keys for keyboard interaction.
2137 * See the [OOUI documentation on MediaWiki] [1] for examples.
2139 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2145 * @param {Object} [config] Configuration options
2146 * @cfg {jQuery} [$button] The button element created by the class.
2147 * If this configuration is omitted, the button element will use a generated `<a>`.
2148 * @cfg {boolean} [framed=true] Render the button with a frame
2150 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2151 // Configuration initialization
2152 config
= config
|| {};
2155 this.$button
= null;
2157 this.active
= config
.active
!== undefined && config
.active
;
2158 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
2159 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2160 this.onDocumentKeyUpHandler
= this.onDocumentKeyUp
.bind( this );
2161 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2162 this.onClickHandler
= this.onClick
.bind( this );
2163 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2166 this.$element
.addClass( 'oo-ui-buttonElement' );
2167 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2168 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2173 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2175 /* Static Properties */
2178 * Cancel mouse down events.
2180 * This property is usually set to `true` to prevent the focus from changing when the button is
2182 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and
2183 * {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} use a value of `false` so that dragging
2184 * behavior is possible and mousedown events can be handled by a parent widget.
2188 * @property {boolean}
2190 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2195 * A 'click' event is emitted when the button element is clicked.
2203 * Set the button element.
2205 * This method is used to retarget a button mixin so that its functionality applies to
2206 * the specified button element instead of the one created by the class. If a button element
2207 * is already set, the method will remove the mixin’s effect on that element.
2209 * @param {jQuery} $button Element to use as button
2211 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2212 if ( this.$button
) {
2214 .removeClass( 'oo-ui-buttonElement-button' )
2215 .removeAttr( 'role accesskey' )
2217 mousedown
: this.onMouseDownHandler
,
2218 keydown
: this.onKeyDownHandler
,
2219 click
: this.onClickHandler
,
2220 keypress
: this.onKeyPressHandler
2224 this.$button
= $button
2225 .addClass( 'oo-ui-buttonElement-button' )
2227 mousedown
: this.onMouseDownHandler
,
2228 keydown
: this.onKeyDownHandler
,
2229 click
: this.onClickHandler
,
2230 keypress
: this.onKeyPressHandler
2233 // Add `role="button"` on `<a>` elements, where it's needed
2234 // `toUpperCase()` is added for XHTML documents
2235 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2236 this.$button
.attr( 'role', 'button' );
2241 * Handles mouse down events.
2244 * @param {jQuery.Event} e Mouse down event
2245 * @return {undefined|boolean} False to prevent default if event is handled
2247 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2248 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2251 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2252 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2253 // reliably remove the pressed class
2254 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2255 // Prevent change of focus unless specifically configured otherwise
2256 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2262 * Handles document mouse up events.
2265 * @param {MouseEvent} e Mouse up event
2267 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentMouseUp = function ( e
) {
2268 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2271 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2272 // Stop listening for mouseup, since we only needed this once
2273 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2277 * Handles mouse click events.
2280 * @param {jQuery.Event} e Mouse click event
2282 * @return {undefined|boolean} False to prevent default if event is handled
2284 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2285 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2286 if ( this.emit( 'click' ) ) {
2293 * Handles key down events.
2296 * @param {jQuery.Event} e Key down event
2298 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2299 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2302 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2303 // Run the keyup handler no matter where the key is when the button is let go, so we can
2304 // reliably remove the pressed class
2305 this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2309 * Handles document key up events.
2312 * @param {KeyboardEvent} e Key up event
2314 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentKeyUp = function ( e
) {
2315 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2318 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2319 // Stop listening for keyup, since we only needed this once
2320 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2324 * Handles key press events.
2327 * @param {jQuery.Event} e Key press event
2329 * @return {undefined|boolean} False to prevent default if event is handled
2331 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2332 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2333 if ( this.emit( 'click' ) ) {
2340 * Check if button has a frame.
2342 * @return {boolean} Button is framed
2344 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2349 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame
2352 * @param {boolean} [framed] Make button framed, omit to toggle
2354 * @return {OO.ui.Element} The element, for chaining
2356 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2357 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2358 if ( framed
!== this.framed
) {
2359 this.framed
= framed
;
2361 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2362 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2363 this.updateThemeClasses();
2370 * Set the button's active state.
2372 * The active state can be set on:
2374 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2375 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2376 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2379 * @param {boolean} value Make button active
2381 * @return {OO.ui.Element} The element, for chaining
2383 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2384 this.active
= !!value
;
2385 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2386 this.updateThemeClasses();
2391 * Check if the button is active
2394 * @return {boolean} The button is active
2396 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2401 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2402 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2403 * items from the group is done through the interface the class provides.
2404 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2406 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2409 * @mixins OO.EmitterList
2413 * @param {Object} [config] Configuration options
2414 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2415 * is omitted, the group element will use a generated `<div>`.
2417 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2418 // Configuration initialization
2419 config
= config
|| {};
2421 // Mixin constructors
2422 OO
.EmitterList
.call( this, config
);
2428 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2433 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2440 * A change event is emitted when the set of selected items changes.
2442 * @param {OO.ui.Element[]} items Items currently in the group
2448 * Set the group element.
2450 * If an element is already set, items will be moved to the new element.
2452 * @param {jQuery} $group Element to use as group
2454 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2457 this.$group
= $group
;
2458 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2459 this.$group
.append( this.items
[ i
].$element
);
2464 * Find an item by its data.
2466 * Only the first item with matching data will be returned. To return all matching items,
2467 * use the #findItemsFromData method.
2469 * @param {Object} data Item data to search for
2470 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2472 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2474 hash
= OO
.getHash( data
);
2476 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2477 item
= this.items
[ i
];
2478 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2487 * Find items by their data.
2489 * All items with matching data will be returned. To return only the first match, use the
2490 * #findItemFromData method instead.
2492 * @param {Object} data Item data to search for
2493 * @return {OO.ui.Element[]} Items with equivalent data
2495 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2497 hash
= OO
.getHash( data
),
2500 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2501 item
= this.items
[ i
];
2502 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2511 * Add items to the group.
2513 * Items will be added to the end of the group array unless the optional `index` parameter
2514 * specifies a different insertion point. Adding an existing item will move it to the end of the
2515 * array or the point specified by the `index`.
2517 * @param {OO.ui.Element[]} items An array of items to add to the group
2518 * @param {number} [index] Index of the insertion point
2520 * @return {OO.ui.Element} The element, for chaining
2522 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2524 if ( items
.length
=== 0 ) {
2529 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2531 this.emit( 'change', this.getItems() );
2538 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2539 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2540 this.insertItemElements( items
, newIndex
);
2543 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2551 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2552 item
.setElementGroup( this );
2553 this.insertItemElements( item
, index
);
2556 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2562 * Insert elements into the group
2565 * @param {OO.ui.Element} itemWidget Item to insert
2566 * @param {number} index Insertion index
2568 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2569 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2570 this.$group
.append( itemWidget
.$element
);
2571 } else if ( index
=== 0 ) {
2572 this.$group
.prepend( itemWidget
.$element
);
2574 this.items
[ index
].$element
.before( itemWidget
.$element
);
2579 * Remove the specified items from a group.
2581 * Removed items are detached (not removed) from the DOM so that they may be reused.
2582 * To remove all items from a group, you may wish to use the #clearItems method instead.
2584 * @param {OO.ui.Element[]} items An array of items to remove
2586 * @return {OO.ui.Element} The element, for chaining
2588 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2589 var i
, len
, item
, index
;
2591 if ( items
.length
=== 0 ) {
2595 // Remove specific items elements
2596 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2598 index
= this.items
.indexOf( item
);
2599 if ( index
!== -1 ) {
2600 item
.setElementGroup( null );
2601 item
.$element
.detach();
2606 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2608 this.emit( 'change', this.getItems() );
2613 * Clear all items from the group.
2615 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2616 * To remove only a subset of items from a group, use the #removeItems method.
2619 * @return {OO.ui.Element} The element, for chaining
2621 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2624 // Remove all item elements
2625 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2626 this.items
[ i
].setElementGroup( null );
2627 this.items
[ i
].$element
.detach();
2631 OO
.EmitterList
.prototype.clearItems
.call( this );
2633 this.emit( 'change', this.getItems() );
2638 * LabelElement is often mixed into other classes to generate a label, which
2639 * helps identify the function of an interface element.
2640 * See the [OOUI documentation on MediaWiki] [1] for more information.
2642 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2648 * @param {Object} [config] Configuration options
2649 * @cfg {jQuery} [$label] The label element created by the class. If this
2650 * configuration is omitted, the label element will use a generated `<span>`.
2651 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be
2652 * specified as a plaintext string, a jQuery selection of elements, or a function that will
2653 * produce a string in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2654 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2655 * @cfg {boolean} [invisibleLabel] Whether the label should be visually hidden (but still
2656 * accessible to screen-readers).
2658 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2659 // Configuration initialization
2660 config
= config
|| {};
2665 this.invisibleLabel
= null;
2668 this.setLabel( config
.label
|| this.constructor.static.label
);
2669 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2670 this.setInvisibleLabel( config
.invisibleLabel
);
2675 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2680 * @event labelChange
2681 * @param {string} value
2684 /* Static Properties */
2687 * The label text. The label can be specified as a plaintext string, a function that will
2688 * produce a string in the future, or `null` for no label. The static value will
2689 * be overridden if a label is specified with the #label config option.
2693 * @property {string|Function|null}
2695 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2697 /* Static methods */
2700 * Highlight the first occurrence of the query in the given text
2702 * @param {string} text Text
2703 * @param {string} query Query to find
2704 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2705 * @return {jQuery} Text with the first match of the query
2706 * sub-string wrapped in highlighted span
2708 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2711 $result
= $( '<span>' );
2715 qLen
= query
.length
;
2716 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2717 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2722 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2725 if ( !query
.length
|| offset
=== -1 ) {
2726 $result
.text( text
);
2729 document
.createTextNode( text
.slice( 0, offset
) ),
2731 .addClass( 'oo-ui-labelElement-label-highlight' )
2732 .text( text
.slice( offset
, offset
+ query
.length
) ),
2733 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2736 return $result
.contents();
2742 * Set the label element.
2744 * If an element is already set, it will be cleaned up before setting up the new element.
2746 * @param {jQuery} $label Element to use as label
2748 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2749 if ( this.$label
) {
2750 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2753 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2754 this.setLabelContent( this.label
);
2760 * An empty string will result in the label being hidden. A string containing only whitespace will
2761 * be converted to a single ` `.
2763 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that
2764 * returns nodes or text; or null for no label
2766 * @return {OO.ui.Element} The element, for chaining
2768 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2769 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2770 label
= ( ( typeof label
=== 'string' || label
instanceof $ ) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2772 if ( this.label
!== label
) {
2773 if ( this.$label
) {
2774 this.setLabelContent( label
);
2777 this.emit( 'labelChange' );
2780 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2786 * Set whether the label should be visually hidden (but still accessible to screen-readers).
2788 * @param {boolean} invisibleLabel
2790 * @return {OO.ui.Element} The element, for chaining
2792 OO
.ui
.mixin
.LabelElement
.prototype.setInvisibleLabel = function ( invisibleLabel
) {
2793 invisibleLabel
= !!invisibleLabel
;
2795 if ( this.invisibleLabel
!== invisibleLabel
) {
2796 this.invisibleLabel
= invisibleLabel
;
2797 this.emit( 'labelChange' );
2800 this.$label
.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel
);
2801 // Pretend that there is no label, a lot of CSS has been written with this assumption
2802 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2808 * Set the label as plain text with a highlighted query
2810 * @param {string} text Text label to set
2811 * @param {string} query Substring of text to highlight
2812 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2814 * @return {OO.ui.Element} The element, for chaining
2816 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
2817 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
2823 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2824 * text; or null for no label
2826 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2831 * Set the content of the label.
2833 * Do not call this method until after the label element has been set by #setLabelElement.
2836 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2837 * text; or null for no label
2839 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2840 if ( typeof label
=== 'string' ) {
2841 if ( label
.match( /^\s*$/ ) ) {
2842 // Convert whitespace only string to a single non-breaking space
2843 this.$label
.html( ' ' );
2845 this.$label
.text( label
);
2847 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2848 this.$label
.html( label
.toString() );
2849 } else if ( label
instanceof $ ) {
2850 this.$label
.empty().append( label
);
2852 this.$label
.empty();
2857 * IconElement is often mixed into other classes to generate an icon.
2858 * Icons are graphics, about the size of normal text. They are used to aid the user
2859 * in locating a control or to convey information in a space-efficient way. See the
2860 * [OOUI documentation on MediaWiki] [1] for a list of icons
2861 * included in the library.
2863 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2869 * @param {Object} [config] Configuration options
2870 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2871 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2872 * the icon element be set to an existing icon instead of the one generated by this class, set a
2873 * value using a jQuery selection. For example:
2875 * // Use a <div> tag instead of a <span>
2876 * $icon: $( '<div>' )
2877 * // Use an existing icon element instead of the one generated by the class
2878 * $icon: this.$element
2879 * // Use an icon element from a child widget
2880 * $icon: this.childwidget.$element
2881 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a
2882 * map of symbolic names. A map is used for i18n purposes and contains a `default` icon
2883 * name and additional names keyed by language code. The `default` name is used when no icon is
2884 * keyed by the user's language.
2886 * Example of an i18n map:
2888 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2889 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2890 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2892 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2893 // Configuration initialization
2894 config
= config
|| {};
2901 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2902 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2907 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2909 /* Static Properties */
2912 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map
2913 * is used for i18n purposes and contains a `default` icon name and additional names keyed by
2914 * language code. The `default` name is used when no icon is keyed by the user's language.
2916 * Example of an i18n map:
2918 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2920 * Note: the static property will be overridden if the #icon configuration is used.
2924 * @property {Object|string}
2926 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2929 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2930 * function that returns title text, or `null` for no title.
2932 * The static property will be overridden if the #iconTitle configuration is used.
2936 * @property {string|Function|null}
2938 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2943 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2944 * applies to the specified icon element instead of the one created by the class. If an icon
2945 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2946 * and mixin methods will no longer affect the element.
2948 * @param {jQuery} $icon Element to use as icon
2950 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2953 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2954 .removeAttr( 'title' );
2958 .addClass( 'oo-ui-iconElement-icon' )
2959 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
2960 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2961 if ( this.iconTitle
!== null ) {
2962 this.$icon
.attr( 'title', this.iconTitle
);
2965 this.updateThemeClasses();
2969 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2970 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2973 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2974 * by language code, or `null` to remove the icon.
2976 * @return {OO.ui.Element} The element, for chaining
2978 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2979 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2980 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2982 if ( this.icon
!== icon
) {
2984 if ( this.icon
!== null ) {
2985 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2987 if ( icon
!== null ) {
2988 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
2994 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
2996 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
2998 this.updateThemeClasses();
3004 * Get the symbolic name of the icon.
3006 * @return {string} Icon name
3008 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
3013 * IndicatorElement is often mixed into other classes to generate an indicator.
3014 * Indicators are small graphics that are generally used in two ways:
3016 * - To draw attention to the status of an item. For example, an indicator might be
3017 * used to show that an item in a list has errors that need to be resolved.
3018 * - To clarify the function of a control that acts in an exceptional way (a button
3019 * that opens a menu instead of performing an action directly, for example).
3021 * For a list of indicators included in the library, please see the
3022 * [OOUI documentation on MediaWiki] [1].
3024 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3030 * @param {Object} [config] Configuration options
3031 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
3032 * configuration is omitted, the indicator element will use a generated `<span>`.
3033 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3034 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3036 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3038 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
3039 // Configuration initialization
3040 config
= config
|| {};
3043 this.$indicator
= null;
3044 this.indicator
= null;
3047 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
3048 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
3053 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
3055 /* Static Properties */
3058 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3059 * The static property will be overridden if the #indicator configuration is used.
3063 * @property {string|null}
3065 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
3068 * A text string used as the indicator title, a function that returns title text, or `null`
3069 * for no title. The static property will be overridden if the #indicatorTitle configuration is
3074 * @property {string|Function|null}
3076 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
3081 * Set the indicator element.
3083 * If an element is already set, it will be cleaned up before setting up the new element.
3085 * @param {jQuery} $indicator Element to use as indicator
3087 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
3088 if ( this.$indicator
) {
3090 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
3091 .removeAttr( 'title' );
3094 this.$indicator
= $indicator
3095 .addClass( 'oo-ui-indicatorElement-indicator' )
3096 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
3097 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
3098 if ( this.indicatorTitle
!== null ) {
3099 this.$indicator
.attr( 'title', this.indicatorTitle
);
3102 this.updateThemeClasses();
3106 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null`
3107 * to remove the indicator.
3109 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3111 * @return {OO.ui.Element} The element, for chaining
3113 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
3114 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
3116 if ( this.indicator
!== indicator
) {
3117 if ( this.$indicator
) {
3118 if ( this.indicator
!== null ) {
3119 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
3121 if ( indicator
!== null ) {
3122 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
3125 this.indicator
= indicator
;
3128 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
3129 if ( this.$indicator
) {
3130 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
3132 this.updateThemeClasses();
3138 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3140 * @return {string} Symbolic name of indicator
3142 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
3143 return this.indicator
;
3147 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3148 * additional functionality to an element created by another class. The class provides
3149 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3150 * which are used to customize the look and feel of a widget to better describe its
3151 * importance and functionality.
3153 * The library currently contains the following styling flags for general use:
3155 * - **progressive**: Progressive styling is applied to convey that the widget will move the user
3156 * forward in a process.
3157 * - **destructive**: Destructive styling is applied to convey that the widget will remove
3160 * The flags affect the appearance of the buttons:
3163 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3164 * var button1 = new OO.ui.ButtonWidget( {
3165 * label: 'Progressive',
3166 * flags: 'progressive'
3168 * button2 = new OO.ui.ButtonWidget( {
3169 * label: 'Destructive',
3170 * flags: 'destructive'
3172 * $( document.body ).append( button1.$element, button2.$element );
3174 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an
3175 * action, use these flags: **primary** and **safe**.
3176 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3178 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3184 * @param {Object} [config] Configuration options
3185 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary')
3187 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3188 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3189 * @cfg {jQuery} [$flagged] The flagged element. By default,
3190 * the flagged functionality is applied to the element created by the class ($element).
3191 * If a different element is specified, the flagged functionality will be applied to it instead.
3193 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3194 // Configuration initialization
3195 config
= config
|| {};
3199 this.$flagged
= null;
3202 this.setFlags( config
.flags
|| this.constructor.static.flags
);
3203 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3208 OO
.initClass( OO
.ui
.mixin
.FlaggedElement
);
3214 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3215 * parameter contains the name of each modified flag and indicates whether it was
3218 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3219 * that the flag was added, `false` that the flag was removed.
3222 /* Static Properties */
3225 * Initial value to pass to setFlags if no value is provided in config.
3229 * @property {string|string[]|Object.<string, boolean>}
3231 OO
.ui
.mixin
.FlaggedElement
.static.flags
= null;
3236 * Set the flagged element.
3238 * This method is used to retarget a flagged mixin so that its functionality applies to the
3239 * specified element.
3240 * If an element is already set, the method will remove the mixin’s effect on that element.
3242 * @param {jQuery} $flagged Element that should be flagged
3244 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3245 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3246 return 'oo-ui-flaggedElement-' + flag
;
3249 if ( this.$flagged
) {
3250 this.$flagged
.removeClass( classNames
);
3253 this.$flagged
= $flagged
.addClass( classNames
);
3257 * Check if the specified flag is set.
3259 * @param {string} flag Name of flag
3260 * @return {boolean} The flag is set
3262 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3263 // This may be called before the constructor, thus before this.flags is set
3264 return this.flags
&& ( flag
in this.flags
);
3268 * Get the names of all flags set.
3270 * @return {string[]} Flag names
3272 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3273 // This may be called before the constructor, thus before this.flags is set
3274 return Object
.keys( this.flags
|| {} );
3281 * @return {OO.ui.Element} The element, for chaining
3284 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3285 var flag
, className
,
3288 classPrefix
= 'oo-ui-flaggedElement-';
3290 for ( flag
in this.flags
) {
3291 className
= classPrefix
+ flag
;
3292 changes
[ flag
] = false;
3293 delete this.flags
[ flag
];
3294 remove
.push( className
);
3297 if ( this.$flagged
) {
3298 this.$flagged
.removeClass( remove
);
3301 this.updateThemeClasses();
3302 this.emit( 'flag', changes
);
3308 * Add one or more flags.
3310 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3311 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3312 * be added (`true`) or removed (`false`).
3314 * @return {OO.ui.Element} The element, for chaining
3317 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3318 var i
, len
, flag
, className
,
3322 classPrefix
= 'oo-ui-flaggedElement-';
3324 if ( typeof flags
=== 'string' ) {
3325 className
= classPrefix
+ flags
;
3327 if ( !this.flags
[ flags
] ) {
3328 this.flags
[ flags
] = true;
3329 add
.push( className
);
3331 } else if ( Array
.isArray( flags
) ) {
3332 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3334 className
= classPrefix
+ flag
;
3336 if ( !this.flags
[ flag
] ) {
3337 changes
[ flag
] = true;
3338 this.flags
[ flag
] = true;
3339 add
.push( className
);
3342 } else if ( OO
.isPlainObject( flags
) ) {
3343 for ( flag
in flags
) {
3344 className
= classPrefix
+ flag
;
3345 if ( flags
[ flag
] ) {
3347 if ( !this.flags
[ flag
] ) {
3348 changes
[ flag
] = true;
3349 this.flags
[ flag
] = true;
3350 add
.push( className
);
3354 if ( this.flags
[ flag
] ) {
3355 changes
[ flag
] = false;
3356 delete this.flags
[ flag
];
3357 remove
.push( className
);
3363 if ( this.$flagged
) {
3366 .removeClass( remove
);
3369 this.updateThemeClasses();
3370 this.emit( 'flag', changes
);
3376 * TitledElement is mixed into other classes to provide a `title` attribute.
3377 * Titles are rendered by the browser and are made visible when the user moves
3378 * the mouse over the element. Titles are not visible on touch devices.
3381 * // TitledElement provides a `title` attribute to the
3382 * // ButtonWidget class.
3383 * var button = new OO.ui.ButtonWidget( {
3384 * label: 'Button with Title',
3385 * title: 'I am a button'
3387 * $( document.body ).append( button.$element );
3393 * @param {Object} [config] Configuration options
3394 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3395 * If this config is omitted, the title functionality is applied to $element, the
3396 * element created by the class.
3397 * @cfg {string|Function} [title] The title text or a function that returns text. If
3398 * this config is omitted, the value of the {@link #static-title static title} property is used.
3400 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3401 // Configuration initialization
3402 config
= config
|| {};
3405 this.$titled
= null;
3409 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3410 this.setTitledElement( config
.$titled
|| this.$element
);
3415 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3417 /* Static Properties */
3420 * The title text, a function that returns text, or `null` for no title. The value of the static
3421 * property is overridden if the #title config option is used.
3423 * If the element has a default title (e.g. `<input type=file>`), `null` will allow that title to be
3424 * shown. Use empty string to suppress it.
3428 * @property {string|Function|null}
3430 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3435 * Set the titled element.
3437 * This method is used to retarget a TitledElement mixin so that its functionality applies to the
3438 * specified element.
3439 * If an element is already set, the mixin’s effect on that element is removed before the new
3440 * element is set up.
3442 * @param {jQuery} $titled Element that should use the 'titled' functionality
3444 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3445 if ( this.$titled
) {
3446 this.$titled
.removeAttr( 'title' );
3449 this.$titled
= $titled
;
3456 * @param {string|Function|null} title Title text, a function that returns text, or `null`
3459 * @return {OO.ui.Element} The element, for chaining
3461 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3462 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3463 title
= typeof title
=== 'string' ? title
: null;
3465 if ( this.title
!== title
) {
3474 * Update the title attribute, in case of changes to title or accessKey.
3478 * @return {OO.ui.Element} The element, for chaining
3480 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3481 var title
= this.getTitle();
3482 if ( this.$titled
) {
3483 if ( title
!== null ) {
3484 // Only if this is an AccessKeyedElement
3485 if ( this.formatTitleWithAccessKey
) {
3486 title
= this.formatTitleWithAccessKey( title
);
3488 this.$titled
.attr( 'title', title
);
3490 this.$titled
.removeAttr( 'title' );
3499 * @return {string} Title string
3501 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3506 * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
3507 * Access keys allow an user to go to a specific element by using
3508 * a shortcut combination of a browser specific keys + the key
3512 * // AccessKeyedElement provides an `accesskey` attribute to the
3513 * // ButtonWidget class.
3514 * var button = new OO.ui.ButtonWidget( {
3515 * label: 'Button with access key',
3518 * $( document.body ).append( button.$element );
3524 * @param {Object} [config] Configuration options
3525 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3526 * If this config is omitted, the access key functionality is applied to $element, the
3527 * element created by the class.
3528 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3529 * this config is omitted, no access key will be added.
3531 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3532 // Configuration initialization
3533 config
= config
|| {};
3536 this.$accessKeyed
= null;
3537 this.accessKey
= null;
3540 this.setAccessKey( config
.accessKey
|| null );
3541 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3543 // If this is also a TitledElement and it initialized before we did, we may have
3544 // to update the title with the access key
3545 if ( this.updateTitle
) {
3552 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3554 /* Static Properties */
3557 * The access key, a function that returns a key, or `null` for no access key.
3561 * @property {string|Function|null}
3563 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3568 * Set the access keyed element.
3570 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to
3571 * the specified element.
3572 * If an element is already set, the mixin's effect on that element is removed before the new
3573 * element is set up.
3575 * @param {jQuery} $accessKeyed Element that should use the 'access keyed' functionality
3577 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3578 if ( this.$accessKeyed
) {
3579 this.$accessKeyed
.removeAttr( 'accesskey' );
3582 this.$accessKeyed
= $accessKeyed
;
3583 if ( this.accessKey
) {
3584 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3591 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no
3594 * @return {OO.ui.Element} The element, for chaining
3596 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3597 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3599 if ( this.accessKey
!== accessKey
) {
3600 if ( this.$accessKeyed
) {
3601 if ( accessKey
!== null ) {
3602 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3604 this.$accessKeyed
.removeAttr( 'accesskey' );
3607 this.accessKey
= accessKey
;
3609 // Only if this is a TitledElement
3610 if ( this.updateTitle
) {
3621 * @return {string} accessKey string
3623 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3624 return this.accessKey
;
3628 * Add information about the access key to the element's tooltip label.
3629 * (This is only public for hacky usage in FieldLayout.)
3631 * @param {string} title Tooltip label for `title` attribute
3634 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3637 if ( !this.$accessKeyed
) {
3638 // Not initialized yet; the constructor will call updateTitle() which will rerun this
3642 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the
3644 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3645 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3647 accessKey
= this.getAccessKey();
3650 title
+= ' [' + accessKey
+ ']';
3656 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3657 * feels, and functionality can be customized via the class’s configuration options
3658 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3661 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3664 * // A button widget.
3665 * var button = new OO.ui.ButtonWidget( {
3666 * label: 'Button with Icon',
3670 * $( document.body ).append( button.$element );
3672 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3675 * @extends OO.ui.Widget
3676 * @mixins OO.ui.mixin.ButtonElement
3677 * @mixins OO.ui.mixin.IconElement
3678 * @mixins OO.ui.mixin.IndicatorElement
3679 * @mixins OO.ui.mixin.LabelElement
3680 * @mixins OO.ui.mixin.TitledElement
3681 * @mixins OO.ui.mixin.FlaggedElement
3682 * @mixins OO.ui.mixin.TabIndexedElement
3683 * @mixins OO.ui.mixin.AccessKeyedElement
3686 * @param {Object} [config] Configuration options
3687 * @cfg {boolean} [active=false] Whether button should be shown as active
3688 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3689 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3690 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3692 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3693 // Configuration initialization
3694 config
= config
|| {};
3696 // Parent constructor
3697 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3699 // Mixin constructors
3700 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3701 OO
.ui
.mixin
.IconElement
.call( this, config
);
3702 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3703 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3704 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
3705 $titled
: this.$button
3707 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3708 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
3709 $tabIndexed
: this.$button
3711 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
3712 $accessKeyed
: this.$button
3718 this.noFollow
= false;
3721 this.connect( this, {
3722 disable
: 'onDisable'
3726 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3728 .addClass( 'oo-ui-buttonWidget' )
3729 .append( this.$button
);
3730 this.setActive( config
.active
);
3731 this.setHref( config
.href
);
3732 this.setTarget( config
.target
);
3733 this.setNoFollow( config
.noFollow
);
3738 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3739 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3740 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3741 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3742 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3743 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3744 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3745 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3746 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3748 /* Static Properties */
3754 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3760 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3765 * Get hyperlink location.
3767 * @return {string} Hyperlink location
3769 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3774 * Get hyperlink target.
3776 * @return {string} Hyperlink target
3778 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3783 * Get search engine traversal hint.
3785 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3787 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3788 return this.noFollow
;
3792 * Set hyperlink location.
3794 * @param {string|null} href Hyperlink location, null to remove
3796 * @return {OO.ui.Widget} The widget, for chaining
3798 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3799 href
= typeof href
=== 'string' ? href
: null;
3800 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3804 if ( href
!== this.href
) {
3813 * Update the `href` attribute, in case of changes to href or
3818 * @return {OO.ui.Widget} The widget, for chaining
3820 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3821 if ( this.href
!== null && !this.isDisabled() ) {
3822 this.$button
.attr( 'href', this.href
);
3824 this.$button
.removeAttr( 'href' );
3831 * Handle disable events.
3834 * @param {boolean} disabled Element is disabled
3836 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3841 * Set hyperlink target.
3843 * @param {string|null} target Hyperlink target, null to remove
3844 * @return {OO.ui.Widget} The widget, for chaining
3846 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3847 target
= typeof target
=== 'string' ? target
: null;
3849 if ( target
!== this.target
) {
3850 this.target
= target
;
3851 if ( target
!== null ) {
3852 this.$button
.attr( 'target', target
);
3854 this.$button
.removeAttr( 'target' );
3862 * Set search engine traversal hint.
3864 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3865 * @return {OO.ui.Widget} The widget, for chaining
3867 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3868 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3870 if ( noFollow
!== this.noFollow
) {
3871 this.noFollow
= noFollow
;
3873 this.$button
.attr( 'rel', 'nofollow' );
3875 this.$button
.removeAttr( 'rel' );
3882 // Override method visibility hints from ButtonElement
3893 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3894 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3895 * removed, and cleared from the group.
3898 * // A ButtonGroupWidget with two buttons.
3899 * var button1 = new OO.ui.PopupButtonWidget( {
3900 * label: 'Select a category',
3903 * $content: $( '<p>List of categories…</p>' ),
3908 * button2 = new OO.ui.ButtonWidget( {
3911 * buttonGroup = new OO.ui.ButtonGroupWidget( {
3912 * items: [ button1, button2 ]
3914 * $( document.body ).append( buttonGroup.$element );
3917 * @extends OO.ui.Widget
3918 * @mixins OO.ui.mixin.GroupElement
3919 * @mixins OO.ui.mixin.TitledElement
3922 * @param {Object} [config] Configuration options
3923 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3925 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3926 // Configuration initialization
3927 config
= config
|| {};
3929 // Parent constructor
3930 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3932 // Mixin constructors
3933 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {
3934 $group
: this.$element
3936 OO
.ui
.mixin
.TitledElement
.call( this, config
);
3939 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3940 if ( Array
.isArray( config
.items
) ) {
3941 this.addItems( config
.items
);
3947 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3948 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3949 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.TitledElement
);
3951 /* Static Properties */
3957 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
3965 * @return {OO.ui.Widget} The widget, for chaining
3967 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
3968 if ( !this.isDisabled() ) {
3969 if ( this.items
[ 0 ] ) {
3970 this.items
[ 0 ].focus();
3979 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
3984 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}.
3985 * In general, IconWidgets should be used with OO.ui.LabelWidget, which creates a label that
3986 * identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
3987 * for a list of icons included in the library.
3990 * // An IconWidget with a label via LabelWidget.
3991 * var myIcon = new OO.ui.IconWidget( {
3995 * // Create a label.
3996 * iconLabel = new OO.ui.LabelWidget( {
3999 * $( document.body ).append( myIcon.$element, iconLabel.$element );
4001 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4004 * @extends OO.ui.Widget
4005 * @mixins OO.ui.mixin.IconElement
4006 * @mixins OO.ui.mixin.TitledElement
4007 * @mixins OO.ui.mixin.LabelElement
4008 * @mixins OO.ui.mixin.FlaggedElement
4011 * @param {Object} [config] Configuration options
4013 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
4014 // Configuration initialization
4015 config
= config
|| {};
4017 // Parent constructor
4018 OO
.ui
.IconWidget
.parent
.call( this, config
);
4020 // Mixin constructors
4021 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {
4022 $icon
: this.$element
4024 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4025 $titled
: this.$element
4027 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4028 $label
: this.$element
,
4029 invisibleLabel
: true
4031 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {
4032 $flagged
: this.$element
4036 this.$element
.addClass( 'oo-ui-iconWidget' );
4037 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4038 // nested in other widgets, because this widget used to not mix in LabelElement.
4039 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4044 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
4045 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
4046 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
4047 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.LabelElement
);
4048 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
4050 /* Static Properties */
4056 OO
.ui
.IconWidget
.static.tagName
= 'span';
4059 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4060 * attention to the status of an item or to clarify the function within a control. For a list of
4061 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4064 * // An indicator widget.
4065 * var indicator1 = new OO.ui.IndicatorWidget( {
4066 * indicator: 'required'
4068 * // Create a fieldset layout to add a label.
4069 * fieldset = new OO.ui.FieldsetLayout();
4070 * fieldset.addItems( [
4071 * new OO.ui.FieldLayout( indicator1, {
4072 * label: 'A required indicator:'
4075 * $( document.body ).append( fieldset.$element );
4077 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4080 * @extends OO.ui.Widget
4081 * @mixins OO.ui.mixin.IndicatorElement
4082 * @mixins OO.ui.mixin.TitledElement
4083 * @mixins OO.ui.mixin.LabelElement
4086 * @param {Object} [config] Configuration options
4088 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4089 // Configuration initialization
4090 config
= config
|| {};
4092 // Parent constructor
4093 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4095 // Mixin constructors
4096 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {
4097 $indicator
: this.$element
4099 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4100 $titled
: this.$element
4102 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4103 $label
: this.$element
,
4104 invisibleLabel
: true
4108 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4109 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4110 // nested in other widgets, because this widget used to not mix in LabelElement.
4111 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4116 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4117 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4118 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4119 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.LabelElement
);
4121 /* Static Properties */
4127 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4130 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4131 * be configured with a `label` option that is set to a string, a label node, or a function:
4133 * - String: a plaintext string
4134 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4135 * label that includes a link or special styling, such as a gray color or additional
4136 * graphical elements.
4137 * - Function: a function that will produce a string in the future. Functions are used
4138 * in cases where the value of the label is not currently defined.
4140 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget},
4141 * which will come into focus when the label is clicked.
4144 * // Two LabelWidgets.
4145 * var label1 = new OO.ui.LabelWidget( {
4146 * label: 'plaintext label'
4148 * label2 = new OO.ui.LabelWidget( {
4149 * label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4151 * // Create a fieldset layout with fields for each example.
4152 * fieldset = new OO.ui.FieldsetLayout();
4153 * fieldset.addItems( [
4154 * new OO.ui.FieldLayout( label1 ),
4155 * new OO.ui.FieldLayout( label2 )
4157 * $( document.body ).append( fieldset.$element );
4160 * @extends OO.ui.Widget
4161 * @mixins OO.ui.mixin.LabelElement
4162 * @mixins OO.ui.mixin.TitledElement
4165 * @param {Object} [config] Configuration options
4166 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4167 * Clicking the label will focus the specified input field.
4169 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4170 // Configuration initialization
4171 config
= config
|| {};
4173 // Parent constructor
4174 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4176 // Mixin constructors
4177 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4178 $label
: this.$element
4180 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4183 this.input
= config
.input
;
4187 if ( this.input
.getInputId() ) {
4188 this.$element
.attr( 'for', this.input
.getInputId() );
4190 this.$label
.on( 'click', function () {
4191 this.input
.simulateLabelClick();
4195 this.$element
.addClass( 'oo-ui-labelWidget' );
4200 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4201 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4202 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4204 /* Static Properties */
4210 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4213 * MessageWidget produces a visual component for sending a notice to the user
4214 * with an icon and distinct design noting its purpose. The MessageWidget changes
4215 * its visual presentation based on the type chosen, which also denotes its UX
4219 * @extends OO.ui.Widget
4220 * @mixins OO.ui.mixin.IconElement
4221 * @mixins OO.ui.mixin.LabelElement
4222 * @mixins OO.ui.mixin.TitledElement
4223 * @mixins OO.ui.mixin.FlaggedElement
4226 * @param {Object} [config] Configuration options
4227 * @cfg {string} [type='notice'] The type of the notice widget. This will also
4228 * impact the flags that the widget receives (and hence its CSS design) as well
4229 * as the icon that appears. Available types:
4230 * 'notice', 'error', 'warning', 'success'
4231 * @cfg {boolean} [inline] Set the notice as an inline notice. The default
4232 * is not inline, or 'boxed' style.
4234 OO
.ui
.MessageWidget
= function OoUiMessageWidget( config
) {
4235 // Configuration initialization
4236 config
= config
|| {};
4238 // Parent constructor
4239 OO
.ui
.MessageWidget
.parent
.call( this, config
);
4241 // Mixin constructors
4242 OO
.ui
.mixin
.IconElement
.call( this, config
);
4243 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4244 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4245 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
4248 this.setType( config
.type
);
4249 this.setInline( config
.inline
);
4253 .append( this.$icon
, this.$label
)
4254 .addClass( 'oo-ui-messageWidget' );
4259 OO
.inheritClass( OO
.ui
.MessageWidget
, OO
.ui
.Widget
);
4260 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.IconElement
);
4261 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.LabelElement
);
4262 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.TitledElement
);
4263 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.FlaggedElement
);
4265 /* Static Properties */
4268 * An object defining the icon name per defined type.
4271 * @property {Object}
4273 OO
.ui
.MessageWidget
.static.iconMap
= {
4274 notice
: 'infoFilled',
4283 * Set the inline state of the widget.
4285 * @param {boolean} inline Widget is inline
4287 OO
.ui
.MessageWidget
.prototype.setInline = function ( inline
) {
4290 if ( this.inline
!== inline
) {
4291 this.inline
= inline
;
4293 .toggleClass( 'oo-ui-messageWidget-block', !this.inline
);
4297 * Set the widget type. The given type must belong to the list of
4298 * legal types set by OO.ui.MessageWidget.static.iconMap
4300 * @param {string} [type] Given type. Defaults to 'notice'
4302 OO
.ui
.MessageWidget
.prototype.setType = function ( type
) {
4304 if ( Object
.keys( this.constructor.static.iconMap
).indexOf( type
) === -1 ) {
4305 type
= 'notice'; // Default
4308 if ( this.type
!== type
) {
4312 this.setFlags( type
);
4314 // Set the icon and its variant
4315 this.setIcon( this.constructor.static.iconMap
[ type
] );
4316 this.$icon
.removeClass( 'oo-ui-image-' + this.type
);
4317 this.$icon
.addClass( 'oo-ui-image-' + type
);
4319 if ( type
=== 'error' ) {
4320 this.$element
.attr( 'role', 'alert' );
4321 this.$element
.removeAttr( 'aria-live' );
4323 this.$element
.removeAttr( 'role' );
4324 this.$element
.attr( 'aria-live', 'polite' );
4332 * PendingElement is a mixin that is used to create elements that notify users that something is
4333 * happening and that they should wait before proceeding. The pending state is visually represented
4334 * with a pending texture that appears in the head of a pending
4335 * {@link OO.ui.ProcessDialog process dialog} or in the input field of a
4336 * {@link OO.ui.TextInputWidget text input widget}.
4338 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked
4339 * as pending, but only when used in {@link OO.ui.MessageDialog message dialogs}. The behavior is
4340 * not currently supported for action widgets used in process dialogs.
4343 * function MessageDialog( config ) {
4344 * MessageDialog.parent.call( this, config );
4346 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4348 * MessageDialog.static.name = 'myMessageDialog';
4349 * MessageDialog.static.actions = [
4350 * { action: 'save', label: 'Done', flags: 'primary' },
4351 * { label: 'Cancel', flags: 'safe' }
4354 * MessageDialog.prototype.initialize = function () {
4355 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4356 * this.content = new OO.ui.PanelLayout( { padded: true } );
4357 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending ' +
4358 * 'state. Note that action widgets can be marked pending in message dialogs but not ' +
4359 * 'process dialogs.</p>' );
4360 * this.$body.append( this.content.$element );
4362 * MessageDialog.prototype.getBodyHeight = function () {
4365 * MessageDialog.prototype.getActionProcess = function ( action ) {
4366 * var dialog = this;
4367 * if ( action === 'save' ) {
4368 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4369 * return new OO.ui.Process()
4371 * .next( function () {
4372 * dialog.getActions().get({actions: 'save'})[0].popPending();
4375 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4378 * var windowManager = new OO.ui.WindowManager();
4379 * $( document.body ).append( windowManager.$element );
4381 * var dialog = new MessageDialog();
4382 * windowManager.addWindows( [ dialog ] );
4383 * windowManager.openWindow( dialog );
4389 * @param {Object} [config] Configuration options
4390 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4392 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4393 // Configuration initialization
4394 config
= config
|| {};
4398 this.$pending
= null;
4401 this.setPendingElement( config
.$pending
|| this.$element
);
4406 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4411 * Set the pending element (and clean up any existing one).
4413 * @param {jQuery} $pending The element to set to pending.
4415 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4416 if ( this.$pending
) {
4417 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4420 this.$pending
= $pending
;
4421 if ( this.pending
> 0 ) {
4422 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4427 * Check if an element is pending.
4429 * @return {boolean} Element is pending
4431 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4432 return !!this.pending
;
4436 * Increase the pending counter. The pending state will remain active until the counter is zero
4437 * (i.e., the number of calls to #pushPending and #popPending is the same).
4440 * @return {OO.ui.Element} The element, for chaining
4442 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4443 if ( this.pending
=== 0 ) {
4444 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4445 this.updateThemeClasses();
4453 * Decrease the pending counter. The pending state will remain active until the counter is zero
4454 * (i.e., the number of calls to #pushPending and #popPending is the same).
4457 * @return {OO.ui.Element} The element, for chaining
4459 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4460 if ( this.pending
=== 1 ) {
4461 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4462 this.updateThemeClasses();
4464 this.pending
= Math
.max( 0, this.pending
- 1 );
4470 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4471 * in the document (for example, in an OO.ui.Window's $overlay).
4473 * The elements's position is automatically calculated and maintained when window is resized or the
4474 * page is scrolled. If you reposition the container manually, you have to call #position to make
4475 * sure the element is still placed correctly.
4477 * As positioning is only possible when both the element and the container are attached to the DOM
4478 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4479 * the #toggle method to display a floating popup, for example.
4485 * @param {Object} [config] Configuration options
4486 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4487 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4488 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4489 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4490 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4491 * 'top': Align the top edge with $floatableContainer's top edge
4492 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4493 * 'center': Vertically align the center with $floatableContainer's center
4494 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4495 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4496 * 'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
4497 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4498 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4499 * 'center': Horizontally align the center with $floatableContainer's center
4500 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4503 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4504 // Configuration initialization
4505 config
= config
|| {};
4508 this.$floatable
= null;
4509 this.$floatableContainer
= null;
4510 this.$floatableWindow
= null;
4511 this.$floatableClosestScrollable
= null;
4512 this.floatableOutOfView
= false;
4513 this.onFloatableScrollHandler
= this.position
.bind( this );
4514 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4517 this.setFloatableContainer( config
.$floatableContainer
);
4518 this.setFloatableElement( config
.$floatable
|| this.$element
);
4519 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4520 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4521 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ?
4522 true : !!config
.hideWhenOutOfView
;
4528 * Set floatable element.
4530 * If an element is already set, it will be cleaned up before setting up the new element.
4532 * @param {jQuery} $floatable Element to make floatable
4534 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4535 if ( this.$floatable
) {
4536 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4537 this.$floatable
.css( { left
: '', top
: '' } );
4540 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4545 * Set floatable container.
4547 * The element will be positioned relative to the specified container.
4549 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4551 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4552 this.$floatableContainer
= $floatableContainer
;
4553 if ( this.$floatable
) {
4559 * Change how the element is positioned vertically.
4561 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4563 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4564 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4565 throw new Error( 'Invalid value for vertical position: ' + position
);
4567 if ( this.verticalPosition
!== position
) {
4568 this.verticalPosition
= position
;
4569 if ( this.$floatable
) {
4576 * Change how the element is positioned horizontally.
4578 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4580 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4581 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4582 throw new Error( 'Invalid value for horizontal position: ' + position
);
4584 if ( this.horizontalPosition
!== position
) {
4585 this.horizontalPosition
= position
;
4586 if ( this.$floatable
) {
4593 * Toggle positioning.
4595 * Do not turn positioning on until after the element is attached to the DOM and visible.
4597 * @param {boolean} [positioning] Enable positioning, omit to toggle
4599 * @return {OO.ui.Element} The element, for chaining
4601 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4602 var closestScrollableOfContainer
;
4604 if ( !this.$floatable
|| !this.$floatableContainer
) {
4608 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4610 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4611 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4612 this.warnedUnattached
= true;
4615 if ( this.positioning
!== positioning
) {
4616 this.positioning
= positioning
;
4618 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer(
4619 this.$floatableContainer
[ 0 ]
4621 // If the scrollable is the root, we have to listen to scroll events
4622 // on the window because of browser inconsistencies.
4623 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4624 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow(
4625 closestScrollableOfContainer
4629 if ( positioning
) {
4630 this.$floatableWindow
= $( this.getElementWindow() );
4631 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4633 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4634 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4636 // Initial position after visible
4639 if ( this.$floatableWindow
) {
4640 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4641 this.$floatableWindow
= null;
4644 if ( this.$floatableClosestScrollable
) {
4645 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4646 this.$floatableClosestScrollable
= null;
4649 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4657 * Check whether the bottom edge of the given element is within the viewport of the given
4661 * @param {jQuery} $element
4662 * @param {jQuery} $container
4665 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4666 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
,
4667 rightEdgeInBounds
, startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4668 direction
= $element
.css( 'direction' );
4670 elemRect
= $element
[ 0 ].getBoundingClientRect();
4671 if ( $container
[ 0 ] === window
) {
4672 viewportSpacing
= OO
.ui
.getViewportSpacing();
4676 right
: document
.documentElement
.clientWidth
,
4677 bottom
: document
.documentElement
.clientHeight
4679 contRect
.top
+= viewportSpacing
.top
;
4680 contRect
.left
+= viewportSpacing
.left
;
4681 contRect
.right
-= viewportSpacing
.right
;
4682 contRect
.bottom
-= viewportSpacing
.bottom
;
4684 contRect
= $container
[ 0 ].getBoundingClientRect();
4687 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4688 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4689 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4690 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4691 if ( direction
=== 'rtl' ) {
4692 startEdgeInBounds
= rightEdgeInBounds
;
4693 endEdgeInBounds
= leftEdgeInBounds
;
4695 startEdgeInBounds
= leftEdgeInBounds
;
4696 endEdgeInBounds
= rightEdgeInBounds
;
4699 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4702 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4705 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4708 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4712 // The other positioning values are all about being inside the container,
4713 // so in those cases all we care about is that any part of the container is visible.
4714 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4715 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4719 * Check if the floatable is hidden to the user because it was offscreen.
4721 * @return {boolean} Floatable is out of view
4723 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4724 return this.floatableOutOfView
;
4728 * Position the floatable below its container.
4730 * This should only be done when both of them are attached to the DOM and visible.
4733 * @return {OO.ui.Element} The element, for chaining
4735 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4736 if ( !this.positioning
) {
4741 // To continue, some things need to be true:
4742 // The element must actually be in the DOM
4743 this.isElementAttached() && (
4744 // The closest scrollable is the current window
4745 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4746 // OR is an element in the element's DOM
4747 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4750 // Abort early if important parts of the widget are no longer attached to the DOM
4754 this.floatableOutOfView
= this.hideWhenOutOfView
&&
4755 !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4756 if ( this.floatableOutOfView
) {
4757 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4760 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4763 this.$floatable
.css( this.computePosition() );
4765 // We updated the position, so re-evaluate the clipping state.
4766 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4767 // will not notice the need to update itself.)
4768 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here.
4769 // Why does it not listen to the right events in the right places?
4778 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4779 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4780 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4782 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4784 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4785 var isBody
, scrollableX
, scrollableY
, containerPos
,
4786 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4787 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4788 direction
= this.$floatableContainer
.css( 'direction' ),
4789 $offsetParent
= this.$floatable
.offsetParent();
4791 if ( $offsetParent
.is( 'html' ) ) {
4792 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4793 // <html> element, but they do work on the <body>
4794 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4796 isBody
= $offsetParent
.is( 'body' );
4797 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' ||
4798 $offsetParent
.css( 'overflow-x' ) === 'auto';
4799 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' ||
4800 $offsetParent
.css( 'overflow-y' ) === 'auto';
4802 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4803 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4804 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container
4805 // is the body, or if it isn't scrollable
4806 scrollTop
= scrollableY
&& !isBody
?
4807 $offsetParent
.scrollTop() : 0;
4808 scrollLeft
= scrollableX
&& !isBody
?
4809 OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4811 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4812 // if the <body> has a margin
4813 containerPos
= isBody
?
4814 this.$floatableContainer
.offset() :
4815 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4816 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4817 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4818 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4819 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4821 if ( this.verticalPosition
=== 'below' ) {
4822 newPos
.top
= containerPos
.bottom
;
4823 } else if ( this.verticalPosition
=== 'above' ) {
4824 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4825 } else if ( this.verticalPosition
=== 'top' ) {
4826 newPos
.top
= containerPos
.top
;
4827 } else if ( this.verticalPosition
=== 'bottom' ) {
4828 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4829 } else if ( this.verticalPosition
=== 'center' ) {
4830 newPos
.top
= containerPos
.top
+
4831 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4834 if ( this.horizontalPosition
=== 'before' ) {
4835 newPos
.end
= containerPos
.start
;
4836 } else if ( this.horizontalPosition
=== 'after' ) {
4837 newPos
.start
= containerPos
.end
;
4838 } else if ( this.horizontalPosition
=== 'start' ) {
4839 newPos
.start
= containerPos
.start
;
4840 } else if ( this.horizontalPosition
=== 'end' ) {
4841 newPos
.end
= containerPos
.end
;
4842 } else if ( this.horizontalPosition
=== 'center' ) {
4843 newPos
.left
= containerPos
.left
+
4844 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4847 if ( newPos
.start
!== undefined ) {
4848 if ( direction
=== 'rtl' ) {
4849 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4850 $offsetParent
).outerWidth() - newPos
.start
;
4852 newPos
.left
= newPos
.start
;
4854 delete newPos
.start
;
4856 if ( newPos
.end
!== undefined ) {
4857 if ( direction
=== 'rtl' ) {
4858 newPos
.left
= newPos
.end
;
4860 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4861 $offsetParent
).outerWidth() - newPos
.end
;
4866 // Account for scroll position
4867 if ( newPos
.top
!== '' ) {
4868 newPos
.top
+= scrollTop
;
4870 if ( newPos
.bottom
!== '' ) {
4871 newPos
.bottom
-= scrollTop
;
4873 if ( newPos
.left
!== '' ) {
4874 newPos
.left
+= scrollLeft
;
4876 if ( newPos
.right
!== '' ) {
4877 newPos
.right
-= scrollLeft
;
4880 // Account for scrollbar gutter
4881 if ( newPos
.bottom
!== '' ) {
4882 newPos
.bottom
-= horizScrollbarHeight
;
4884 if ( direction
=== 'rtl' ) {
4885 if ( newPos
.left
!== '' ) {
4886 newPos
.left
-= vertScrollbarWidth
;
4889 if ( newPos
.right
!== '' ) {
4890 newPos
.right
-= vertScrollbarWidth
;
4898 * Element that can be automatically clipped to visible boundaries.
4900 * Whenever the element's natural height changes, you have to call
4901 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4902 * clipping correctly.
4904 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4905 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4906 * then #$clippable will be given a fixed reduced height and/or width and will be made
4907 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4908 * but you can build a static footer by setting #$clippableContainer to an element that contains
4909 * #$clippable and the footer.
4915 * @param {Object} [config] Configuration options
4916 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4917 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4918 * omit to use #$clippable
4920 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4921 // Configuration initialization
4922 config
= config
|| {};
4925 this.$clippable
= null;
4926 this.$clippableContainer
= null;
4927 this.clipping
= false;
4928 this.clippedHorizontally
= false;
4929 this.clippedVertically
= false;
4930 this.$clippableScrollableContainer
= null;
4931 this.$clippableScroller
= null;
4932 this.$clippableWindow
= null;
4933 this.idealWidth
= null;
4934 this.idealHeight
= null;
4935 this.onClippableScrollHandler
= this.clip
.bind( this );
4936 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4939 if ( config
.$clippableContainer
) {
4940 this.setClippableContainer( config
.$clippableContainer
);
4942 this.setClippableElement( config
.$clippable
|| this.$element
);
4948 * Set clippable element.
4950 * If an element is already set, it will be cleaned up before setting up the new element.
4952 * @param {jQuery} $clippable Element to make clippable
4954 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4955 if ( this.$clippable
) {
4956 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4957 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4958 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4961 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4966 * Set clippable container.
4968 * This is the container that will be measured when deciding whether to clip. When clipping,
4969 * #$clippable will be resized in order to keep the clippable container fully visible.
4971 * If the clippable container is unset, #$clippable will be used.
4973 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4975 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4976 this.$clippableContainer
= $clippableContainer
;
4977 if ( this.$clippable
) {
4985 * Do not turn clipping on until after the element is attached to the DOM and visible.
4987 * @param {boolean} [clipping] Enable clipping, omit to toggle
4989 * @return {OO.ui.Element} The element, for chaining
4991 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4992 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4994 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4995 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4996 this.warnedUnattached
= true;
4999 if ( this.clipping
!== clipping
) {
5000 this.clipping
= clipping
;
5002 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
5003 // If the clippable container is the root, we have to listen to scroll events and check
5004 // jQuery.scrollTop on the window because of browser inconsistencies
5005 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
5006 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
5007 this.$clippableScrollableContainer
;
5008 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
5009 this.$clippableWindow
= $( this.getElementWindow() )
5010 .on( 'resize', this.onClippableWindowResizeHandler
);
5011 // Initial clip after visible
5014 this.$clippable
.css( {
5022 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5024 this.$clippableScrollableContainer
= null;
5025 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
5026 this.$clippableScroller
= null;
5027 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
5028 this.$clippableWindow
= null;
5036 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5038 * @return {boolean} Element will be clipped to the visible area
5040 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
5041 return this.clipping
;
5045 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5047 * @return {boolean} Part of the element is being clipped
5049 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
5050 return this.clippedHorizontally
|| this.clippedVertically
;
5054 * Check if the right of the element is being clipped by the nearest scrollable container.
5056 * @return {boolean} Part of the element is being clipped
5058 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
5059 return this.clippedHorizontally
;
5063 * Check if the bottom of the element is being clipped by the nearest scrollable container.
5065 * @return {boolean} Part of the element is being clipped
5067 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
5068 return this.clippedVertically
;
5072 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
5074 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
5075 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
5077 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
5078 this.idealWidth
= width
;
5079 this.idealHeight
= height
;
5081 if ( !this.clipping
) {
5082 // Update dimensions
5083 this.$clippable
.css( { width
: width
, height
: height
} );
5085 // While clipping, idealWidth and idealHeight are not considered
5089 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5090 * ClippableElement will clip the opposite side when reducing element's width.
5092 * Classes that mix in ClippableElement should override this to return 'right' if their
5093 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
5094 * If your class also mixes in FloatableElement, this is handled automatically.
5096 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5097 * always in pixels, even if they were unset or set to 'auto'.)
5099 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
5101 * @return {string} 'left' or 'right'
5103 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
5104 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
5111 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5112 * ClippableElement will clip the opposite side when reducing element's width.
5114 * Classes that mix in ClippableElement should override this to return 'bottom' if their
5115 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
5116 * If your class also mixes in FloatableElement, this is handled automatically.
5118 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5119 * always in pixels, even if they were unset or set to 'auto'.)
5121 * When in doubt, 'top' is a sane fallback.
5123 * @return {string} 'top' or 'bottom'
5125 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
5126 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
5133 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5134 * when the element's natural height changes.
5136 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5137 * overlapped by, the visible area of the nearest scrollable container.
5139 * Because calling clip() when the natural height changes isn't always possible, we also set
5140 * max-height when the element isn't being clipped. This means that if the element tries to grow
5141 * beyond the edge, something reasonable will happen before clip() is called.
5144 * @return {OO.ui.Element} The element, for chaining
5146 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
5147 var extraHeight
, extraWidth
, viewportSpacing
,
5148 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
5149 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
5150 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
5151 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
5152 // Extra tolerance so that the sloppy code below doesn't result in results that are off
5153 // by one or two pixels. (And also so that we have space to display drop shadows.)
5154 // Chosen by fair dice roll.
5157 if ( !this.clipping
) {
5158 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below
5163 function rectIntersection( a
, b
) {
5165 out
.top
= Math
.max( a
.top
, b
.top
);
5166 out
.left
= Math
.max( a
.left
, b
.left
);
5167 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
5168 out
.right
= Math
.min( a
.right
, b
.right
);
5172 viewportSpacing
= OO
.ui
.getViewportSpacing();
5174 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
5175 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
5176 // Dimensions of the browser window, rather than the element!
5180 right
: document
.documentElement
.clientWidth
,
5181 bottom
: document
.documentElement
.clientHeight
5183 viewportRect
.top
+= viewportSpacing
.top
;
5184 viewportRect
.left
+= viewportSpacing
.left
;
5185 viewportRect
.right
-= viewportSpacing
.right
;
5186 viewportRect
.bottom
-= viewportSpacing
.bottom
;
5188 $viewport
= this.$clippableScrollableContainer
;
5189 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
5190 // Convert into a plain object
5191 viewportRect
= $.extend( {}, viewportRect
);
5194 // Account for scrollbar gutter
5195 direction
= $viewport
.css( 'direction' );
5196 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
5197 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
5198 viewportRect
.bottom
-= horizScrollbarHeight
;
5199 if ( direction
=== 'rtl' ) {
5200 viewportRect
.left
+= vertScrollbarWidth
;
5202 viewportRect
.right
-= vertScrollbarWidth
;
5205 // Add arbitrary tolerance
5206 viewportRect
.top
+= buffer
;
5207 viewportRect
.left
+= buffer
;
5208 viewportRect
.right
-= buffer
;
5209 viewportRect
.bottom
-= buffer
;
5211 $item
= this.$clippableContainer
|| this.$clippable
;
5213 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
5214 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
5216 itemRect
= $item
[ 0 ].getBoundingClientRect();
5217 // Convert into a plain object
5218 itemRect
= $.extend( {}, itemRect
);
5220 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5221 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5222 if ( this.getHorizontalAnchorEdge() === 'right' ) {
5223 itemRect
.left
= viewportRect
.left
;
5225 itemRect
.right
= viewportRect
.right
;
5227 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5228 itemRect
.top
= viewportRect
.top
;
5230 itemRect
.bottom
= viewportRect
.bottom
;
5233 availableRect
= rectIntersection( viewportRect
, itemRect
);
5235 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5236 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5237 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5238 desiredWidth
= Math
.min( desiredWidth
,
5239 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5240 desiredHeight
= Math
.min( desiredHeight
,
5241 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5242 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5243 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5244 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5245 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5246 clipWidth
= allotedWidth
< naturalWidth
;
5247 clipHeight
= allotedHeight
< naturalHeight
;
5250 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5252 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5254 this.$clippable
.css( 'overflowX', 'scroll' );
5255 // eslint-disable-next-line no-void
5256 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5257 this.$clippable
.css( {
5258 width
: Math
.max( 0, allotedWidth
),
5262 this.$clippable
.css( {
5264 width
: this.idealWidth
|| '',
5265 maxWidth
: Math
.max( 0, allotedWidth
)
5269 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5271 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5273 this.$clippable
.css( 'overflowY', 'scroll' );
5274 // eslint-disable-next-line no-void
5275 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5276 this.$clippable
.css( {
5277 height
: Math
.max( 0, allotedHeight
),
5281 this.$clippable
.css( {
5283 height
: this.idealHeight
|| '',
5284 maxHeight
: Math
.max( 0, allotedHeight
)
5288 // If we stopped clipping in at least one of the dimensions
5289 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5290 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5293 this.clippedHorizontally
= clipWidth
;
5294 this.clippedVertically
= clipHeight
;
5300 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5301 * By default, each popup has an anchor that points toward its origin.
5302 * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
5304 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5308 * var popup = new OO.ui.PopupWidget( {
5309 * $content: $( '<p>Hi there!</p>' ),
5314 * $( document.body ).append( popup.$element );
5315 * // To display the popup, toggle the visibility to 'true'.
5316 * popup.toggle( true );
5318 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5321 * @extends OO.ui.Widget
5322 * @mixins OO.ui.mixin.LabelElement
5323 * @mixins OO.ui.mixin.ClippableElement
5324 * @mixins OO.ui.mixin.FloatableElement
5327 * @param {Object} [config] Configuration options
5328 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5329 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5330 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5331 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5332 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5333 * of $floatableContainer
5334 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5335 * of $floatableContainer
5336 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5337 * endwards (right/left) to the vertical center of $floatableContainer
5338 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5339 * startwards (left/right) to the vertical center of $floatableContainer
5340 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5341 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in
5342 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5343 * move the popup as far downwards as possible.
5344 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in
5345 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5346 * move the popup as far upwards as possible.
5347 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the
5348 * center of the popup with the center of $floatableContainer.
5349 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5350 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5351 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5352 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5353 * desired direction to display the popup without clipping
5354 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5355 * See the [OOUI docs on MediaWiki][3] for an example.
5356 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5357 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a
5359 * @cfg {jQuery} [$content] Content to append to the popup's body
5360 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5361 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5362 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5363 * This config option is only relevant if #autoClose is set to `true`. See the
5364 * [OOUI documentation on MediaWiki][2] for an example.
5365 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5366 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5368 * @cfg {boolean} [padded=false] Add padding to the popup's body
5370 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5371 // Configuration initialization
5372 config
= config
|| {};
5374 // Parent constructor
5375 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5377 // Properties (must be set before ClippableElement constructor call)
5378 this.$body
= $( '<div>' );
5379 this.$popup
= $( '<div>' );
5381 // Mixin constructors
5382 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5383 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {
5384 $clippable
: this.$body
,
5385 $clippableContainer
: this.$popup
5387 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5390 this.$anchor
= $( '<div>' );
5391 // If undefined, will be computed lazily in computePosition()
5392 this.$container
= config
.$container
;
5393 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5394 this.autoClose
= !!config
.autoClose
;
5395 this.transitionTimeout
= null;
5396 this.anchored
= false;
5397 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5398 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5401 this.setSize( config
.width
, config
.height
);
5402 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5403 this.setAlignment( config
.align
|| 'center' );
5404 this.setPosition( config
.position
|| 'below' );
5405 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5406 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5407 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5408 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5410 .addClass( 'oo-ui-popupWidget-popup' )
5411 .append( this.$body
);
5413 .addClass( 'oo-ui-popupWidget' )
5414 .append( this.$popup
, this.$anchor
);
5415 // Move content, which was added to #$element by OO.ui.Widget, to the body
5416 // FIXME This is gross, we should use '$body' or something for the config
5417 if ( config
.$content
instanceof $ ) {
5418 this.$body
.append( config
.$content
);
5421 if ( config
.padded
) {
5422 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5425 if ( config
.head
) {
5426 this.closeButton
= new OO
.ui
.ButtonWidget( {
5430 this.closeButton
.connect( this, {
5431 click
: 'onCloseButtonClick'
5433 this.$head
= $( '<div>' )
5434 .addClass( 'oo-ui-popupWidget-head' )
5435 .append( this.$label
, this.closeButton
.$element
);
5436 this.$popup
.prepend( this.$head
);
5439 if ( config
.$footer
) {
5440 this.$footer
= $( '<div>' )
5441 .addClass( 'oo-ui-popupWidget-footer' )
5442 .append( config
.$footer
);
5443 this.$popup
.append( this.$footer
);
5446 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5447 // that reference properties not initialized at that time of parent class construction
5448 // TODO: Find a better way to handle post-constructor setup
5449 this.visible
= false;
5450 this.$element
.addClass( 'oo-ui-element-hidden' );
5455 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5456 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5457 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5458 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5465 * The popup is ready: it is visible and has been positioned and clipped.
5471 * Handles document mouse down events.
5474 * @param {MouseEvent} e Mouse down event
5476 OO
.ui
.PopupWidget
.prototype.onDocumentMouseDown = function ( e
) {
5479 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5481 this.toggle( false );
5486 * Bind document mouse down listener.
5490 OO
.ui
.PopupWidget
.prototype.bindDocumentMouseDownListener = function () {
5491 // Capture clicks outside popup
5492 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5493 // We add 'click' event because iOS safari needs to respond to this event.
5494 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5495 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5496 // of occasionally not emitting 'click' properly, that event seems to be the standard
5497 // that it should be emitting, so we add it to this and will operate the event handler
5498 // on whichever of these events was triggered first
5499 this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5503 * Handles close button click events.
5507 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5508 if ( this.isVisible() ) {
5509 this.toggle( false );
5514 * Unbind document mouse down listener.
5518 OO
.ui
.PopupWidget
.prototype.unbindDocumentMouseDownListener = function () {
5519 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5520 this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5524 * Handles document key down events.
5527 * @param {KeyboardEvent} e Key down event
5529 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5531 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5534 this.toggle( false );
5536 e
.stopPropagation();
5541 * Bind document key down listener.
5545 OO
.ui
.PopupWidget
.prototype.bindDocumentKeyDownListener = function () {
5546 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5550 * Unbind document key down listener.
5554 OO
.ui
.PopupWidget
.prototype.unbindDocumentKeyDownListener = function () {
5555 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5559 * Show, hide, or toggle the visibility of the anchor.
5561 * @param {boolean} [show] Show anchor, omit to toggle
5563 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5564 show
= show
=== undefined ? !this.anchored
: !!show
;
5566 if ( this.anchored
!== show
) {
5568 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5569 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5571 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5572 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5574 this.anchored
= show
;
5579 * Change which edge the anchor appears on.
5581 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5583 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5584 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5585 throw new Error( 'Invalid value for edge: ' + edge
);
5587 if ( this.anchorEdge
!== null ) {
5588 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5590 this.anchorEdge
= edge
;
5591 if ( this.anchored
) {
5592 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5597 * Check if the anchor is visible.
5599 * @return {boolean} Anchor is visible
5601 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5602 return this.anchored
;
5606 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5607 * `.toggle( true )` after its #$element is attached to the DOM.
5609 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5610 * it in the right place and with the right dimensions only work correctly while it is attached.
5611 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5612 * strictly enforced, so currently it only generates a warning in the browser console.
5617 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5618 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5619 show
= show
=== undefined ? !this.isVisible() : !!show
;
5621 change
= show
!== this.isVisible();
5623 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5624 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5625 this.warnedUnattached
= true;
5627 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5628 // Fall back to the parent node if the floatableContainer is not set
5629 this.setFloatableContainer( this.$element
.parent() );
5632 if ( change
&& show
&& this.autoFlip
) {
5633 // Reset auto-flipping before showing the popup again. It's possible we no longer need to
5634 // flip (e.g. if the user scrolled).
5635 this.isAutoFlipped
= false;
5639 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5642 this.togglePositioning( show
&& !!this.$floatableContainer
);
5645 if ( this.autoClose
) {
5646 this.bindDocumentMouseDownListener();
5647 this.bindDocumentKeyDownListener();
5649 this.updateDimensions();
5650 this.toggleClipping( true );
5652 if ( this.autoFlip
) {
5653 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5654 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5655 // If opening the popup in the normal direction causes it to be clipped,
5656 // open in the opposite one instead
5657 normalHeight
= this.$element
.height();
5658 this.isAutoFlipped
= !this.isAutoFlipped
;
5660 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5661 // If that also causes it to be clipped, open in whichever direction
5662 // we have more space
5663 oppositeHeight
= this.$element
.height();
5664 if ( oppositeHeight
< normalHeight
) {
5665 this.isAutoFlipped
= !this.isAutoFlipped
;
5671 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5672 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5673 // If opening the popup in the normal direction causes it to be clipped,
5674 // open in the opposite one instead
5675 normalWidth
= this.$element
.width();
5676 this.isAutoFlipped
= !this.isAutoFlipped
;
5677 // Due to T180173 horizontally clipped PopupWidgets have messed up
5678 // dimensions, which causes positioning to be off. Toggle clipping back and
5679 // forth to work around.
5680 this.toggleClipping( false );
5682 this.toggleClipping( true );
5683 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5684 // If that also causes it to be clipped, open in whichever direction
5685 // we have more space
5686 oppositeWidth
= this.$element
.width();
5687 if ( oppositeWidth
< normalWidth
) {
5688 this.isAutoFlipped
= !this.isAutoFlipped
;
5689 // Due to T180173, horizontally clipped PopupWidgets have messed up
5690 // dimensions, which causes positioning to be off. Toggle clipping
5691 // back and forth to work around.
5692 this.toggleClipping( false );
5694 this.toggleClipping( true );
5701 this.emit( 'ready' );
5703 this.toggleClipping( false );
5704 if ( this.autoClose
) {
5705 this.unbindDocumentMouseDownListener();
5706 this.unbindDocumentKeyDownListener();
5715 * Set the size of the popup.
5717 * Changing the size may also change the popup's position depending on the alignment.
5719 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5720 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5721 * @param {boolean} [transition=false] Use a smooth transition
5724 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5725 this.width
= width
!== undefined ? width
: 320;
5726 this.height
= height
!== undefined ? height
: null;
5727 if ( this.isVisible() ) {
5728 this.updateDimensions( transition
);
5733 * Update the size and position.
5735 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5736 * be called automatically.
5738 * @param {boolean} [transition=false] Use a smooth transition
5741 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5744 // Prevent transition from being interrupted
5745 clearTimeout( this.transitionTimeout
);
5747 // Enable transition
5748 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5754 // Prevent transitioning after transition is complete
5755 this.transitionTimeout
= setTimeout( function () {
5756 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5759 // Prevent transitioning immediately
5760 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5767 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5768 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
,
5769 anchorPos
, anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
,
5770 floatablePos
, offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5772 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5773 popupPositionOppositeMap
= {
5781 'force-left': 'backwards',
5782 'force-right': 'forwards'
5785 'force-left': 'forwards',
5786 'force-right': 'backwards'
5798 backwards
: this.anchored
? 'before' : 'end'
5806 if ( !this.$container
) {
5807 // Lazy-initialize $container if not specified in constructor
5808 this.$container
= $( this.getClosestScrollableElementContainer() );
5810 direction
= this.$container
.css( 'direction' );
5812 // Set height and width before we do anything else, since it might cause our measurements
5813 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5815 width
: this.width
!== null ? this.width
: 'auto',
5816 height
: this.height
!== null ? this.height
: 'auto'
5819 align
= alignMap
[ direction
][ this.align
] || this.align
;
5820 popupPosition
= this.popupPosition
;
5821 if ( this.isAutoFlipped
) {
5822 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5825 // If the popup is positioned before or after, then the anchor positioning is vertical,
5826 // otherwise horizontal
5827 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5828 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5829 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5830 near
= vertical
? 'top' : 'left';
5831 far
= vertical
? 'bottom' : 'right';
5832 sizeProp
= vertical
? 'Height' : 'Width';
5833 popupSize
= vertical
?
5834 ( this.height
|| this.$popup
.height() ) :
5835 ( this.width
|| this.$popup
.width() );
5837 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5838 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5839 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5842 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5843 // Find out which property FloatableElement used for positioning, and adjust that value
5844 positionProp
= vertical
?
5845 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5846 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5848 // Figure out where the near and far edges of the popup and $floatableContainer are
5849 floatablePos
= this.$floatableContainer
.offset();
5850 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5851 // Measure where the offsetParent is and compute our position based on that and parentPosition
5852 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5853 { top
: 0, left
: 0 } :
5854 this.$element
.offsetParent().offset();
5856 if ( positionProp
=== near
) {
5857 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5858 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5860 popupPos
[ far
] = offsetParentPos
[ near
] +
5861 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5862 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5865 if ( this.anchored
) {
5866 // Position the anchor (which is positioned relative to the popup) to point to
5867 // $floatableContainer
5868 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5869 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5871 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more
5872 // space this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use
5873 // scrollWidth/Height
5874 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5875 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5876 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5877 // Not enough space for the anchor on the start side; pull the popup startwards
5878 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5879 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5880 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5881 // Not enough space for the anchor on the end side; pull the popup endwards
5882 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5883 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5885 positionAdjustment
= 0;
5888 positionAdjustment
= 0;
5891 // Check if the popup will go beyond the edge of this.$container
5892 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5893 { top
: 0, left
: 0 } :
5894 this.$container
.offset();
5895 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5896 if ( this.$container
[ 0 ] === document
.documentElement
) {
5897 viewportSpacing
= OO
.ui
.getViewportSpacing();
5898 containerPos
[ near
] += viewportSpacing
[ near
];
5899 containerPos
[ far
] -= viewportSpacing
[ far
];
5901 // Take into account how much the popup will move because of the adjustments we're going to make
5902 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5903 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5904 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5905 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5906 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5907 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5908 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5909 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5910 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5911 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5914 if ( this.anchored
) {
5915 // Adjust anchorOffset for positionAdjustment
5916 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5918 // Position the anchor
5919 anchorCss
[ start
] = anchorOffset
;
5920 this.$anchor
.css( anchorCss
);
5923 // Move the popup if needed
5924 parentPosition
[ positionProp
] += positionAdjustment
;
5926 return parentPosition
;
5930 * Set popup alignment
5932 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5933 * `backwards` or `forwards`.
5935 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5936 // Validate alignment
5937 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5940 this.align
= 'center';
5946 * Get popup alignment
5948 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5949 * `backwards` or `forwards`.
5951 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5956 * Change the positioning of the popup.
5958 * @param {string} position 'above', 'below', 'before' or 'after'
5960 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5961 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5964 this.popupPosition
= position
;
5969 * Get popup positioning.
5971 * @return {string} 'above', 'below', 'before' or 'after'
5973 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5974 return this.popupPosition
;
5978 * Set popup auto-flipping.
5980 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5981 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5982 * desired direction to display the popup without clipping
5984 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5985 autoFlip
= !!autoFlip
;
5987 if ( this.autoFlip
!== autoFlip
) {
5988 this.autoFlip
= autoFlip
;
5993 * Set which elements will not close the popup when clicked.
5995 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
5997 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
5999 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
6000 this.$autoCloseIgnore
= $autoCloseIgnore
;
6004 * Get an ID of the body element, this can be used as the
6005 * `aria-describedby` attribute for an input field.
6007 * @return {string} The ID of the body element
6009 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
6010 var id
= this.$body
.attr( 'id' );
6011 if ( id
=== undefined ) {
6012 id
= OO
.ui
.generateElementId();
6013 this.$body
.attr( 'id', id
);
6019 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6020 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6021 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6022 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6028 * @param {Object} [config] Configuration options
6029 * @cfg {Object} [popup] Configuration to pass to popup
6030 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6032 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
6033 // Configuration initialization
6034 config
= config
|| {};
6037 this.popup
= new OO
.ui
.PopupWidget( $.extend(
6040 $floatableContainer
: this.$element
6044 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
6054 * @return {OO.ui.PopupWidget} Popup widget
6056 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
6061 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
6062 * which is used to display additional information or options.
6065 * // A PopupButtonWidget.
6066 * var popupButton = new OO.ui.PopupButtonWidget( {
6067 * label: 'Popup button with options',
6070 * $content: $( '<p>Additional options here.</p>' ),
6072 * align: 'force-left'
6075 * // Append the button to the DOM.
6076 * $( document.body ).append( popupButton.$element );
6079 * @extends OO.ui.ButtonWidget
6080 * @mixins OO.ui.mixin.PopupElement
6083 * @param {Object} [config] Configuration options
6084 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful
6085 * in cases where the expanded popup is larger than its containing `<div>`. The specified overlay
6086 * layer is usually on top of the containing `<div>` and has a larger area. By default, the popup
6087 * uses relative positioning.
6088 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
6090 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
6091 // Configuration initialization
6092 config
= config
|| {};
6094 // Parent constructor
6095 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
6097 // Mixin constructors
6098 OO
.ui
.mixin
.PopupElement
.call( this, config
);
6101 this.$overlay
= ( config
.$overlay
=== true ?
6102 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
6105 this.connect( this, {
6110 this.$element
.addClass( 'oo-ui-popupButtonWidget' );
6112 .addClass( 'oo-ui-popupButtonWidget-popup' )
6113 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6114 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6115 this.$overlay
.append( this.popup
.$element
);
6120 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
6121 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
6126 * Handle the button action being triggered.
6130 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
6131 this.popup
.toggle();
6135 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6137 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6142 * @mixins OO.ui.mixin.GroupElement
6145 * @param {Object} [config] Configuration options
6147 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
6148 // Mixin constructors
6149 OO
.ui
.mixin
.GroupElement
.call( this, config
);
6154 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
6159 * Set the disabled state of the widget.
6161 * This will also update the disabled state of child widgets.
6163 * @param {boolean} disabled Disable widget
6165 * @return {OO.ui.Widget} The widget, for chaining
6167 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
6171 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6172 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
6174 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6176 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6177 this.items
[ i
].updateDisabled();
6185 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6187 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group.
6188 * This allows bidirectional communication.
6190 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6198 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
6205 * Check if widget is disabled.
6207 * Checks parent if present, making disabled state inheritable.
6209 * @return {boolean} Widget is disabled
6211 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
6212 return this.disabled
||
6213 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
6217 * Set group element is in.
6219 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6221 * @return {OO.ui.Widget} The widget, for chaining
6223 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
6225 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6226 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
6228 // Initialize item disabled states
6229 this.updateDisabled();
6235 * OptionWidgets are special elements that can be selected and configured with data. The
6236 * data is often unique for each option, but it does not have to be. OptionWidgets are used
6237 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6238 * and examples, please see the [OOUI documentation on MediaWiki][1].
6240 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6243 * @extends OO.ui.Widget
6244 * @mixins OO.ui.mixin.ItemWidget
6245 * @mixins OO.ui.mixin.LabelElement
6246 * @mixins OO.ui.mixin.FlaggedElement
6247 * @mixins OO.ui.mixin.AccessKeyedElement
6248 * @mixins OO.ui.mixin.TitledElement
6251 * @param {Object} [config] Configuration options
6253 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
6254 // Configuration initialization
6255 config
= config
|| {};
6257 // Parent constructor
6258 OO
.ui
.OptionWidget
.parent
.call( this, config
);
6260 // Mixin constructors
6261 OO
.ui
.mixin
.ItemWidget
.call( this );
6262 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6263 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6264 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6265 OO
.ui
.mixin
.TitledElement
.call( this, config
);
6268 this.highlighted
= false;
6269 this.pressed
= false;
6270 this.setSelected( !!config
.selected
);
6274 .data( 'oo-ui-optionWidget', this )
6275 // Allow programmatic focussing (and by access key), but not tabbing
6276 .attr( 'tabindex', '-1' )
6277 .attr( 'role', 'option' )
6278 .addClass( 'oo-ui-optionWidget' )
6279 .append( this.$label
);
6284 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6285 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6286 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6287 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6288 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6289 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.TitledElement
);
6291 /* Static Properties */
6294 * Whether this option can be selected. See #setSelected.
6298 * @property {boolean}
6300 OO
.ui
.OptionWidget
.static.selectable
= true;
6303 * Whether this option can be highlighted. See #setHighlighted.
6307 * @property {boolean}
6309 OO
.ui
.OptionWidget
.static.highlightable
= true;
6312 * Whether this option can be pressed. See #setPressed.
6316 * @property {boolean}
6318 OO
.ui
.OptionWidget
.static.pressable
= true;
6321 * Whether this option will be scrolled into view when it is selected.
6325 * @property {boolean}
6327 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6332 * Check if the option can be selected.
6334 * @return {boolean} Item is selectable
6336 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6337 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6341 * Check if the option can be highlighted. A highlight indicates that the option
6342 * may be selected when a user presses Enter key or clicks. Disabled items cannot
6345 * @return {boolean} Item is highlightable
6347 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6348 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6352 * Check if the option can be pressed. The pressed state occurs when a user mouses
6353 * down on an item, but has not yet let go of the mouse.
6355 * @return {boolean} Item is pressable
6357 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6358 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6362 * Check if the option is selected.
6364 * @return {boolean} Item is selected
6366 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6367 return this.selected
;
6371 * Check if the option is highlighted. A highlight indicates that the
6372 * item may be selected when a user presses Enter key or clicks.
6374 * @return {boolean} Item is highlighted
6376 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6377 return this.highlighted
;
6381 * Check if the option is pressed. The pressed state occurs when a user mouses
6382 * down on an item, but has not yet let go of the mouse. The item may appear
6383 * selected, but it will not be selected until the user releases the mouse.
6385 * @return {boolean} Item is pressed
6387 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6388 return this.pressed
;
6392 * Set the option’s selected state. In general, all modifications to the selection
6393 * should be handled by the SelectWidget’s
6394 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
6396 * @param {boolean} [state=false] Select option
6398 * @return {OO.ui.Widget} The widget, for chaining
6400 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6401 if ( this.constructor.static.selectable
) {
6402 this.selected
= !!state
;
6404 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6405 .attr( 'aria-selected', state
.toString() );
6406 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6407 this.scrollElementIntoView();
6409 this.updateThemeClasses();
6415 * Set the option’s highlighted state. In general, all programmatic
6416 * modifications to the highlight should be handled by the
6417 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6418 * method instead of this method.
6420 * @param {boolean} [state=false] Highlight option
6422 * @return {OO.ui.Widget} The widget, for chaining
6424 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6425 if ( this.constructor.static.highlightable
) {
6426 this.highlighted
= !!state
;
6427 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6428 this.updateThemeClasses();
6434 * Set the option’s pressed state. In general, all
6435 * programmatic modifications to the pressed state should be handled by the
6436 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6437 * method instead of this method.
6439 * @param {boolean} [state=false] Press option
6441 * @return {OO.ui.Widget} The widget, for chaining
6443 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6444 if ( this.constructor.static.pressable
) {
6445 this.pressed
= !!state
;
6446 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6447 this.updateThemeClasses();
6453 * Get text to match search strings against.
6455 * The default implementation returns the label text, but subclasses
6456 * can override this to provide more complex behavior.
6458 * @return {string|boolean} String to match search string against
6460 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6461 var label
= this.getLabel();
6462 return typeof label
=== 'string' ? label
: this.$label
.text();
6466 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6467 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6468 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6471 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For
6472 * more information, please see the [OOUI documentation on MediaWiki][1].
6475 * // A select widget with three options.
6476 * var select = new OO.ui.SelectWidget( {
6478 * new OO.ui.OptionWidget( {
6480 * label: 'Option One',
6482 * new OO.ui.OptionWidget( {
6484 * label: 'Option Two',
6486 * new OO.ui.OptionWidget( {
6488 * label: 'Option Three',
6492 * $( document.body ).append( select.$element );
6494 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6498 * @extends OO.ui.Widget
6499 * @mixins OO.ui.mixin.GroupWidget
6502 * @param {Object} [config] Configuration options
6503 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6504 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6505 * the [OOUI documentation on MediaWiki] [2] for examples.
6506 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6507 * @cfg {boolean} [multiselect] Allow for multiple selections
6509 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6510 // Configuration initialization
6511 config
= config
|| {};
6513 // Parent constructor
6514 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6516 // Mixin constructors
6517 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {
6518 $group
: this.$element
6522 this.pressed
= false;
6523 this.selecting
= null;
6524 this.multiselect
= !!config
.multiselect
;
6525 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
6526 this.onDocumentMouseMoveHandler
= this.onDocumentMouseMove
.bind( this );
6527 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
6528 this.onDocumentKeyPressHandler
= this.onDocumentKeyPress
.bind( this );
6529 this.keyPressBuffer
= '';
6530 this.keyPressBufferTimer
= null;
6531 this.blockMouseOverEvents
= 0;
6534 this.connect( this, {
6538 focusin
: this.onFocus
.bind( this ),
6539 mousedown
: this.onMouseDown
.bind( this ),
6540 mouseover
: this.onMouseOver
.bind( this ),
6541 mouseleave
: this.onMouseLeave
.bind( this )
6546 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-unpressed' )
6547 .attr( 'role', 'listbox' );
6548 this.setFocusOwner( this.$element
);
6549 if ( Array
.isArray( config
.items
) ) {
6550 this.addItems( config
.items
);
6556 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6557 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6564 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6566 * @param {OO.ui.OptionWidget|null} item Highlighted item
6572 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6573 * pressed state of an option.
6575 * @param {OO.ui.OptionWidget|null} item Pressed item
6581 * A `select` event is emitted when the selection is modified programmatically with the #selectItem
6584 * @param {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} items Currently selected items
6590 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6592 * @param {OO.ui.OptionWidget} item Chosen item
6593 * @param {boolean} selected Item is selected
6599 * An `add` event is emitted when options are added to the select with the #addItems method.
6601 * @param {OO.ui.OptionWidget[]} items Added items
6602 * @param {number} index Index of insertion point
6608 * A `remove` event is emitted when options are removed from the select with the #clearItems
6609 * or #removeItems methods.
6611 * @param {OO.ui.OptionWidget[]} items Removed items
6614 /* Static methods */
6617 * Normalize text for filter matching
6619 * @param {string} text Text
6620 * @return {string} Normalized text
6622 OO
.ui
.SelectWidget
.static.normalizeForMatching = function ( text
) {
6623 // Replace trailing whitespace, normalize multiple spaces and make case insensitive
6624 var normalized
= text
.trim().replace( /\s+/, ' ' ).toLowerCase();
6626 // Normalize Unicode
6627 // eslint-disable-next-line no-restricted-properties
6628 if ( normalized
.normalize
) {
6629 // eslint-disable-next-line no-restricted-properties
6630 normalized
= normalized
.normalize();
6638 * Handle focus events
6641 * @param {jQuery.Event} event
6643 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6645 if ( event
.target
=== this.$element
[ 0 ] ) {
6646 // This widget was focussed, e.g. by the user tabbing to it.
6647 // The styles for focus state depend on one of the items being selected.
6648 if ( !this.findSelectedItem() ) {
6649 item
= this.findFirstSelectableItem();
6652 if ( event
.target
.tabIndex
=== -1 ) {
6653 // One of the options got focussed (and the event bubbled up here).
6654 // They can't be tabbed to, but they can be activated using access keys.
6655 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6656 item
= this.findTargetItem( event
);
6658 // There is something actually user-focusable in one of the labels of the options, and
6659 // the user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change
6666 if ( item
.constructor.static.highlightable
) {
6667 this.highlightItem( item
);
6669 this.selectItem( item
);
6673 if ( event
.target
!== this.$element
[ 0 ] ) {
6674 this.$focusOwner
.trigger( 'focus' );
6679 * Handle mouse down events.
6682 * @param {jQuery.Event} e Mouse down event
6683 * @return {undefined|boolean} False to prevent default if event is handled
6685 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6688 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6689 this.togglePressed( true );
6690 item
= this.findTargetItem( e
);
6691 if ( item
&& item
.isSelectable() ) {
6692 this.pressItem( item
);
6693 this.selecting
= item
;
6694 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6695 this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6702 * Handle document mouse up events.
6705 * @param {MouseEvent} e Mouse up event
6706 * @return {undefined|boolean} False to prevent default if event is handled
6708 OO
.ui
.SelectWidget
.prototype.onDocumentMouseUp = function ( e
) {
6711 this.togglePressed( false );
6712 if ( !this.selecting
) {
6713 item
= this.findTargetItem( e
);
6714 if ( item
&& item
.isSelectable() ) {
6715 this.selecting
= item
;
6718 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6719 this.pressItem( null );
6720 this.chooseItem( this.selecting
);
6721 this.selecting
= null;
6724 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6725 this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6731 * Handle document mouse move events.
6734 * @param {MouseEvent} e Mouse move event
6736 OO
.ui
.SelectWidget
.prototype.onDocumentMouseMove = function ( e
) {
6739 if ( !this.isDisabled() && this.pressed
) {
6740 item
= this.findTargetItem( e
);
6741 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6742 this.pressItem( item
);
6743 this.selecting
= item
;
6749 * Handle mouse over events.
6752 * @param {jQuery.Event} e Mouse over event
6753 * @return {undefined|boolean} False to prevent default if event is handled
6755 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6757 if ( this.blockMouseOverEvents
) {
6760 if ( !this.isDisabled() ) {
6761 item
= this.findTargetItem( e
);
6762 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6768 * Handle mouse leave events.
6771 * @param {jQuery.Event} e Mouse over event
6772 * @return {undefined|boolean} False to prevent default if event is handled
6774 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6775 if ( !this.isDisabled() ) {
6776 this.highlightItem( null );
6782 * Handle document key down events.
6785 * @param {KeyboardEvent} e Key down event
6787 OO
.ui
.SelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
6790 selected
= this.findSelectedItems(),
6791 currentItem
= this.findHighlightedItem() || (
6792 Array
.isArray( selected
) ? selected
[ 0 ] : selected
6794 firstItem
= this.getItems()[ 0 ];
6796 if ( !this.isDisabled() && this.isVisible() ) {
6797 switch ( e
.keyCode
) {
6798 case OO
.ui
.Keys
.ENTER
:
6799 if ( currentItem
) {
6800 // Was only highlighted, now let's select it. No-op if already selected.
6801 this.chooseItem( currentItem
);
6806 case OO
.ui
.Keys
.LEFT
:
6807 this.clearKeyPressBuffer();
6808 nextItem
= currentItem
?
6809 this.findRelativeSelectableItem( currentItem
, -1 ) : firstItem
;
6812 case OO
.ui
.Keys
.DOWN
:
6813 case OO
.ui
.Keys
.RIGHT
:
6814 this.clearKeyPressBuffer();
6815 nextItem
= currentItem
?
6816 this.findRelativeSelectableItem( currentItem
, 1 ) : firstItem
;
6819 case OO
.ui
.Keys
.ESCAPE
:
6820 case OO
.ui
.Keys
.TAB
:
6821 if ( currentItem
) {
6822 currentItem
.setHighlighted( false );
6824 this.unbindDocumentKeyDownListener();
6825 this.unbindDocumentKeyPressListener();
6826 // Don't prevent tabbing away / defocusing
6832 if ( nextItem
.constructor.static.highlightable
) {
6833 this.highlightItem( nextItem
);
6835 this.chooseItem( nextItem
);
6837 this.scrollItemIntoView( nextItem
);
6842 e
.stopPropagation();
6848 * Bind document key down listener.
6852 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyDownListener = function () {
6853 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6857 * Unbind document key down listener.
6861 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
6862 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6866 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6868 * @param {OO.ui.OptionWidget} item Item to scroll into view
6870 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6872 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic
6873 // scrolling and around 100-150 ms after it is finished.
6874 this.blockMouseOverEvents
++;
6875 item
.scrollElementIntoView().done( function () {
6876 setTimeout( function () {
6877 widget
.blockMouseOverEvents
--;
6883 * Clear the key-press buffer
6887 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6888 if ( this.keyPressBufferTimer
) {
6889 clearTimeout( this.keyPressBufferTimer
);
6890 this.keyPressBufferTimer
= null;
6892 this.keyPressBuffer
= '';
6896 * Handle key press events.
6899 * @param {KeyboardEvent} e Key press event
6900 * @return {undefined|boolean} False to prevent default if event is handled
6902 OO
.ui
.SelectWidget
.prototype.onDocumentKeyPress = function ( e
) {
6903 var c
, filter
, item
, selected
;
6905 if ( !e
.charCode
) {
6906 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6907 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6912 // eslint-disable-next-line no-restricted-properties
6913 if ( String
.fromCodePoint
) {
6914 // eslint-disable-next-line no-restricted-properties
6915 c
= String
.fromCodePoint( e
.charCode
);
6917 c
= String
.fromCharCode( e
.charCode
);
6920 if ( this.keyPressBufferTimer
) {
6921 clearTimeout( this.keyPressBufferTimer
);
6923 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6925 selected
= this.findSelectedItems();
6926 item
= this.findHighlightedItem() || (
6927 Array
.isArray( selected
) ? selected
[ 0 ] : selected
6930 if ( this.keyPressBuffer
=== c
) {
6931 // Common (if weird) special case: typing "xxxx" will cycle through all
6932 // the items beginning with "x".
6934 item
= this.findRelativeSelectableItem( item
, 1 );
6937 this.keyPressBuffer
+= c
;
6940 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6941 if ( !item
|| !filter( item
) ) {
6942 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6945 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6946 this.highlightItem( item
);
6948 this.chooseItem( item
);
6950 this.scrollItemIntoView( item
);
6954 e
.stopPropagation();
6958 * Get a matcher for the specific string
6961 * @param {string} query String to match against items
6962 * @param {string} [mode='prefix'] Matching mode: 'substring', 'prefix', or 'exact'
6963 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6965 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( query
, mode
) {
6966 var normalizeForMatching
= this.constructor.static.normalizeForMatching
,
6967 normalizedQuery
= normalizeForMatching( query
);
6969 // Support deprecated exact=true argument
6970 if ( mode
=== true ) {
6974 return function ( item
) {
6975 var matchText
= normalizeForMatching( item
.getMatchText() );
6977 if ( normalizedQuery
=== '' ) {
6978 // Empty string matches all, except if we are in 'exact'
6979 // mode, where it doesn't match at all
6980 return mode
!== 'exact';
6985 return matchText
=== normalizedQuery
;
6987 return matchText
.indexOf( normalizedQuery
) !== -1;
6990 return matchText
.indexOf( normalizedQuery
) === 0;
6996 * Bind document key press listener.
7000 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7001 this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
7005 * Unbind document key down listener.
7007 * If you override this, be sure to call this.clearKeyPressBuffer() from your
7012 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7013 this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
7014 this.clearKeyPressBuffer();
7018 * Visibility change handler
7021 * @param {boolean} visible
7023 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
7025 this.clearKeyPressBuffer();
7030 * Get the closest item to a jQuery.Event.
7033 * @param {jQuery.Event} e
7034 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
7036 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
7037 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
7038 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
7041 return $option
.data( 'oo-ui-optionWidget' ) || null;
7045 * Find all selected items, if there are any. If the widget allows for multiselect
7046 * it will return an array of selected options. If the widget doesn't allow for
7047 * multiselect, it will return the selected option or null if no item is selected.
7049 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
7050 * then return an array of selected items (or empty array),
7051 * if the widget is not multiselect, return a single selected item, or `null`
7052 * if no item is selected
7054 OO
.ui
.SelectWidget
.prototype.findSelectedItems = function () {
7055 var selected
= this.items
.filter( function ( item
) {
7056 return item
.isSelected();
7059 return this.multiselect
?
7061 selected
[ 0 ] || null;
7065 * Find selected item.
7067 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
7068 * then return an array of selected items (or empty array),
7069 * if the widget is not multiselect, return a single selected item, or `null`
7070 * if no item is selected
7072 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
7073 return this.findSelectedItems();
7077 * Find highlighted item.
7079 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
7081 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
7084 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7085 if ( this.items
[ i
].isHighlighted() ) {
7086 return this.items
[ i
];
7093 * Toggle pressed state.
7095 * Press is a state that occurs when a user mouses down on an item, but
7096 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
7097 * until the user releases the mouse.
7099 * @param {boolean} pressed An option is being pressed
7101 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
7102 if ( pressed
=== undefined ) {
7103 pressed
= !this.pressed
;
7105 if ( pressed
!== this.pressed
) {
7107 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
7108 .toggleClass( 'oo-ui-selectWidget-unpressed', !pressed
);
7109 this.pressed
= pressed
;
7114 * Highlight an option. If the `item` param is omitted, no options will be highlighted
7115 * and any existing highlight will be removed. The highlight is mutually exclusive.
7117 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
7120 * @return {OO.ui.Widget} The widget, for chaining
7122 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
7123 var i
, len
, highlighted
,
7126 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7127 highlighted
= this.items
[ i
] === item
;
7128 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
7129 this.items
[ i
].setHighlighted( highlighted
);
7135 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7137 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7139 this.emit( 'highlight', item
);
7146 * Fetch an item by its label.
7148 * @param {string} label Label of the item to select.
7149 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7150 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7152 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
7154 len
= this.items
.length
,
7155 filter
= this.getItemMatcher( label
, 'exact' );
7157 for ( i
= 0; i
< len
; i
++ ) {
7158 item
= this.items
[ i
];
7159 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7166 filter
= this.getItemMatcher( label
, 'prefix' );
7167 for ( i
= 0; i
< len
; i
++ ) {
7168 item
= this.items
[ i
];
7169 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7185 * Programmatically select an option by its label. If the item does not exist,
7186 * all options will be deselected.
7188 * @param {string} [label] Label of the item to select.
7189 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7192 * @return {OO.ui.Widget} The widget, for chaining
7194 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
7195 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
7196 if ( label
=== undefined || !itemFromLabel
) {
7197 return this.selectItem();
7199 return this.selectItem( itemFromLabel
);
7203 * Programmatically select an option by its data. If the `data` parameter is omitted,
7204 * or if the item does not exist, all options will be deselected.
7206 * @param {Object|string} [data] Value of the item to select, omit to deselect all
7209 * @return {OO.ui.Widget} The widget, for chaining
7211 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
7212 var itemFromData
= this.findItemFromData( data
);
7213 if ( data
=== undefined || !itemFromData
) {
7214 return this.selectItem();
7216 return this.selectItem( itemFromData
);
7220 * Programmatically unselect an option by its reference. If the widget
7221 * allows for multiple selections, there may be other items still selected;
7222 * otherwise, no items will be selected.
7223 * If no item is given, all selected items will be unselected.
7225 * @param {OO.ui.OptionWidget} [item] Item to unselect
7228 * @return {OO.ui.Widget} The widget, for chaining
7230 OO
.ui
.SelectWidget
.prototype.unselectItem = function ( item
) {
7232 item
.setSelected( false );
7234 this.items
.forEach( function ( item
) {
7235 item
.setSelected( false );
7239 this.emit( 'select', this.findSelectedItems() );
7244 * Programmatically select an option by its reference. If the `item` parameter is omitted,
7245 * all options will be deselected.
7247 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7250 * @return {OO.ui.Widget} The widget, for chaining
7252 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
7253 var i
, len
, selected
,
7256 if ( this.multiselect
&& item
) {
7257 // Select the item directly
7258 item
.setSelected( true );
7260 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7261 selected
= this.items
[ i
] === item
;
7262 if ( this.items
[ i
].isSelected() !== selected
) {
7263 this.items
[ i
].setSelected( selected
);
7269 // TODO: When should a non-highlightable element be selected?
7270 if ( item
&& !item
.constructor.static.highlightable
) {
7272 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7274 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7277 this.emit( 'select', this.findSelectedItems() );
7286 * Press is a state that occurs when a user mouses down on an item, but has not
7287 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7288 * releases the mouse.
7290 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7293 * @return {OO.ui.Widget} The widget, for chaining
7295 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
7296 var i
, len
, pressed
,
7299 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7300 pressed
= this.items
[ i
] === item
;
7301 if ( this.items
[ i
].isPressed() !== pressed
) {
7302 this.items
[ i
].setPressed( pressed
);
7307 this.emit( 'press', item
);
7316 * Note that ‘choose’ should never be modified programmatically. A user can choose
7317 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7318 * use the #selectItem method.
7320 * This method is identical to #selectItem, but may vary in subclasses that take additional action
7321 * when users choose an item with the keyboard or mouse.
7323 * @param {OO.ui.OptionWidget} item Item to choose
7326 * @return {OO.ui.Widget} The widget, for chaining
7328 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
7330 if ( this.multiselect
&& item
.isSelected() ) {
7331 this.unselectItem( item
);
7333 this.selectItem( item
);
7336 this.emit( 'choose', item
, item
.isSelected() );
7343 * Find an option by its position relative to the specified item (or to the start of the option
7344 * array, if item is `null`). The direction in which to search through the option array is specified
7345 * with a number: -1 for reverse (the default) or 1 for forward. The method will return an option,
7346 * or `null` if there are no options in the array.
7348 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at
7349 * the beginning of the array.
7350 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7351 * @param {Function} [filter] Only consider items for which this function returns
7352 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
7353 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
7355 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
7356 var currentIndex
, nextIndex
, i
,
7357 increase
= direction
> 0 ? 1 : -1,
7358 len
= this.items
.length
;
7360 if ( item
instanceof OO
.ui
.OptionWidget
) {
7361 currentIndex
= this.items
.indexOf( item
);
7362 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7364 // If no item is selected and moving forward, start at the beginning.
7365 // If moving backward, start at the end.
7366 nextIndex
= direction
> 0 ? 0 : len
- 1;
7369 for ( i
= 0; i
< len
; i
++ ) {
7370 item
= this.items
[ nextIndex
];
7372 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7373 ( !filter
|| filter( item
) )
7377 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7383 * Find the next selectable item or `null` if there are no selectable items.
7384 * Disabled options and menu-section markers and breaks are not selectable.
7386 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7388 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7389 return this.findRelativeSelectableItem( null, 1 );
7393 * Add an array of options to the select. Optionally, an index number can be used to
7394 * specify an insertion point.
7396 * @param {OO.ui.OptionWidget[]} items Items to add
7397 * @param {number} [index] Index to insert items after
7400 * @return {OO.ui.Widget} The widget, for chaining
7402 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7404 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7406 // Always provide an index, even if it was omitted
7407 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7413 * Remove the specified array of options from the select. Options will be detached
7414 * from the DOM, not removed, so they can be reused later. To remove all options from
7415 * the select, you may wish to use the #clearItems method instead.
7417 * @param {OO.ui.OptionWidget[]} items Items to remove
7420 * @return {OO.ui.Widget} The widget, for chaining
7422 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7425 // Deselect items being removed
7426 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7428 if ( item
.isSelected() ) {
7429 this.selectItem( null );
7434 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7436 this.emit( 'remove', items
);
7442 * Clear all options from the select. Options will be detached from the DOM, not removed,
7443 * so that they can be reused later. To remove a subset of options from the select, use
7444 * the #removeItems method.
7448 * @return {OO.ui.Widget} The widget, for chaining
7450 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7451 var items
= this.items
.slice();
7454 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7457 this.selectItem( null );
7459 this.emit( 'remove', items
);
7465 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7467 * This is used to set `aria-activedescendant` and `aria-expanded` on it.
7470 * @param {jQuery} $focusOwner
7472 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7473 this.$focusOwner
= $focusOwner
;
7477 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7478 * with an {@link OO.ui.mixin.IconElement icon} and/or
7479 * {@link OO.ui.mixin.IndicatorElement indicator}.
7480 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7481 * options. For more information about options and selects, please see the
7482 * [OOUI documentation on MediaWiki][1].
7485 * // Decorated options in a select widget.
7486 * var select = new OO.ui.SelectWidget( {
7488 * new OO.ui.DecoratedOptionWidget( {
7490 * label: 'Option with icon',
7493 * new OO.ui.DecoratedOptionWidget( {
7495 * label: 'Option with indicator',
7500 * $( document.body ).append( select.$element );
7502 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7505 * @extends OO.ui.OptionWidget
7506 * @mixins OO.ui.mixin.IconElement
7507 * @mixins OO.ui.mixin.IndicatorElement
7510 * @param {Object} [config] Configuration options
7512 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7513 // Parent constructor
7514 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7516 // Mixin constructors
7517 OO
.ui
.mixin
.IconElement
.call( this, config
);
7518 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7522 .addClass( 'oo-ui-decoratedOptionWidget' )
7523 .prepend( this.$icon
)
7524 .append( this.$indicator
);
7529 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7530 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7531 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7534 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7535 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7536 * the [OOUI documentation on MediaWiki] [1] for more information.
7538 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7541 * @extends OO.ui.DecoratedOptionWidget
7544 * @param {Object} [config] Configuration options
7546 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7547 // Parent constructor
7548 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7551 this.checkIcon
= new OO
.ui
.IconWidget( {
7553 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7558 .prepend( this.checkIcon
.$element
)
7559 .addClass( 'oo-ui-menuOptionWidget' );
7564 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7566 /* Static Properties */
7572 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7575 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to
7576 * group one or more related {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets
7577 * cannot be highlighted or selected.
7580 * var dropdown = new OO.ui.DropdownWidget( {
7583 * new OO.ui.MenuSectionOptionWidget( {
7586 * new OO.ui.MenuOptionWidget( {
7588 * label: 'Welsh Corgi'
7590 * new OO.ui.MenuOptionWidget( {
7592 * label: 'Standard Poodle'
7594 * new OO.ui.MenuSectionOptionWidget( {
7597 * new OO.ui.MenuOptionWidget( {
7604 * $( document.body ).append( dropdown.$element );
7607 * @extends OO.ui.DecoratedOptionWidget
7610 * @param {Object} [config] Configuration options
7612 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7613 // Parent constructor
7614 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7618 .addClass( 'oo-ui-menuSectionOptionWidget' )
7619 .removeAttr( 'role aria-selected' );
7620 this.selected
= false;
7625 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7627 /* Static Properties */
7633 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7639 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7642 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7643 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7644 * See {@link OO.ui.DropdownWidget DropdownWidget},
7645 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}, and
7646 * {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7647 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7648 * and customized to be opened, closed, and displayed as needed.
7650 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7651 * mouse outside the menu.
7653 * Menus also have support for keyboard interaction:
7655 * - Enter/Return key: choose and select a menu option
7656 * - Up-arrow key: highlight the previous menu option
7657 * - Down-arrow key: highlight the next menu option
7658 * - Escape key: hide the menu
7660 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7662 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7663 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7666 * @extends OO.ui.SelectWidget
7667 * @mixins OO.ui.mixin.ClippableElement
7668 * @mixins OO.ui.mixin.FloatableElement
7671 * @param {Object} [config] Configuration options
7672 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu
7673 * items that match the text the user types. This config is used by
7674 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget} and
7675 * {@link OO.ui.mixin.LookupElement LookupElement}
7676 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7677 * the text the user types. This config is used by
7678 * {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
7679 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks
7680 * the mouse anywhere on the page outside of this widget, the menu is hidden. For example, if
7681 * there is a button that toggles the menu's visibility on click, the menu will be hidden then
7682 * re-shown when the user clicks that button, unless the button (or its parent widget) is passed
7684 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7685 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7686 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7687 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7688 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7689 * @cfg {string} [filterMode='prefix'] The mode by which the menu filters the results.
7690 * Options are 'exact', 'prefix' or 'substring'. See `OO.ui.SelectWidget#getItemMatcher`
7691 * @param {number|string} [width] Width of the menu as a number of pixels or CSS string with unit
7692 * suffix, used by {@link OO.ui.mixin.ClippableElement ClippableElement}
7694 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7695 // Configuration initialization
7696 config
= config
|| {};
7698 // Parent constructor
7699 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7701 // Mixin constructors
7702 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( { $clippable
: this.$group
}, config
) );
7703 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7705 // Initial vertical positions other than 'center' will result in
7706 // the menu being flipped if there is not enough space in the container.
7707 // Store the original position so we know what to reset to.
7708 this.originalVerticalPosition
= this.verticalPosition
;
7711 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7712 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7713 this.filterFromInput
= !!config
.filterFromInput
;
7714 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7715 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7716 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7717 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7718 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7719 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7720 this.lastHighlightedItem
= null;
7721 this.width
= config
.width
;
7722 this.filterMode
= config
.filterMode
;
7725 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7726 if ( config
.widget
) {
7727 this.setFocusOwner( config
.widget
.$tabIndexed
);
7730 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7731 // that reference properties not initialized at that time of parent class construction
7732 // TODO: Find a better way to handle post-constructor setup
7733 this.visible
= false;
7734 this.$element
.addClass( 'oo-ui-element-hidden' );
7735 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7740 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7741 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7742 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7749 * The menu is ready: it is visible and has been positioned and clipped.
7752 /* Static properties */
7755 * Positions to flip to if there isn't room in the container for the
7756 * menu in a specific direction.
7758 * @property {Object.<string,string>}
7760 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7770 * Handles document mouse down events.
7773 * @param {MouseEvent} e Mouse down event
7775 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7779 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7784 this.toggle( false );
7791 OO
.ui
.MenuSelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
7792 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7794 if ( !this.isDisabled() && this.isVisible() ) {
7795 switch ( e
.keyCode
) {
7796 case OO
.ui
.Keys
.LEFT
:
7797 case OO
.ui
.Keys
.RIGHT
:
7798 // Do nothing if a text field is associated, arrow keys will be handled natively
7799 if ( !this.$input
) {
7800 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7803 case OO
.ui
.Keys
.ESCAPE
:
7804 case OO
.ui
.Keys
.TAB
:
7805 if ( currentItem
&& !this.multiselect
) {
7806 currentItem
.setHighlighted( false );
7808 this.toggle( false );
7809 // Don't prevent tabbing away, prevent defocusing
7810 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7812 e
.stopPropagation();
7816 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7823 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7824 * or after items were added/removed (always).
7828 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7829 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7831 len
= this.items
.length
,
7832 showAll
= !this.isVisible(),
7835 if ( this.$input
&& this.filterFromInput
) {
7836 filter
= showAll
? null : this.getItemMatcher( this.$input
.val(), this.filterMode
);
7837 exactFilter
= this.getItemMatcher( this.$input
.val(), 'exact' );
7838 // Hide non-matching options, and also hide section headers if all options
7839 // in their section are hidden.
7840 for ( i
= 0; i
< len
; i
++ ) {
7841 item
= this.items
[ i
];
7842 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7844 // If the previous section was empty, hide its header
7845 section
.toggle( showAll
|| !sectionEmpty
);
7848 sectionEmpty
= true;
7849 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7850 visible
= showAll
|| filter( item
);
7851 exactMatch
= exactMatch
|| exactFilter( item
);
7852 anyVisible
= anyVisible
|| visible
;
7853 sectionEmpty
= sectionEmpty
&& !visible
;
7854 item
.toggle( visible
);
7857 // Process the final section
7859 section
.toggle( showAll
|| !sectionEmpty
);
7862 if ( !anyVisible
) {
7863 this.highlightItem( null );
7866 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7869 this.highlightOnFilter
&&
7870 !( this.lastHighlightedItem
&& this.lastHighlightedItem
.isVisible() )
7872 // Highlight the first item on the list
7874 items
= this.getItems();
7875 for ( i
= 0; i
< items
.length
; i
++ ) {
7876 if ( items
[ i
].isVisible() ) {
7881 this.highlightItem( item
);
7882 this.lastHighlightedItem
= item
;
7887 // Reevaluate clipping
7894 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyDownListener = function () {
7895 if ( this.$input
) {
7896 this.$input
.on( 'keydown', this.onDocumentKeyDownHandler
);
7898 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyDownListener
.call( this );
7905 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
7906 if ( this.$input
) {
7907 this.$input
.off( 'keydown', this.onDocumentKeyDownHandler
);
7909 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyDownListener
.call( this );
7916 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7917 if ( this.$input
) {
7918 if ( this.filterFromInput
) {
7920 'keydown mouseup cut paste change input select',
7921 this.onInputEditHandler
7923 this.updateItemVisibility();
7926 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyPressListener
.call( this );
7933 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7934 if ( this.$input
) {
7935 if ( this.filterFromInput
) {
7937 'keydown mouseup cut paste change input select',
7938 this.onInputEditHandler
7940 this.updateItemVisibility();
7943 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyPressListener
.call( this );
7950 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is
7953 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with
7954 * the keyboard or mouse and it becomes selected. To select an item programmatically,
7955 * use the #selectItem method.
7957 * @param {OO.ui.OptionWidget} item Item to choose
7959 * @return {OO.ui.Widget} The widget, for chaining
7961 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7962 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7963 if ( this.hideOnChoose
) {
7964 this.toggle( false );
7972 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7974 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7976 this.updateItemVisibility();
7984 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7986 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7988 this.updateItemVisibility();
7996 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7998 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
8000 this.updateItemVisibility();
8006 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
8007 * `.toggle( true )` after its #$element is attached to the DOM.
8009 * Do not show the menu while it is not attached to the DOM. The calculations required to display
8010 * it in the right place and with the right dimensions only work correctly while it is attached.
8011 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
8012 * strictly enforced, so currently it only generates a warning in the browser console.
8017 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
8018 var change
, originalHeight
, flippedHeight
, selectedItem
;
8020 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
8021 change
= visible
!== this.isVisible();
8023 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
8024 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
8025 this.warnedUnattached
= true;
8028 if ( change
&& visible
) {
8029 // Reset position before showing the popup again. It's possible we no longer need to flip
8030 // (e.g. if the user scrolled).
8031 this.setVerticalPosition( this.originalVerticalPosition
);
8035 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
8041 this.setIdealSize( this.width
);
8042 } else if ( this.$floatableContainer
) {
8043 this.$clippable
.css( 'width', 'auto' );
8045 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
8046 // Dropdown is smaller than handle so expand to width
8047 this.$floatableContainer
[ 0 ].offsetWidth
:
8048 // Dropdown is larger than handle so auto size
8051 this.$clippable
.css( 'width', '' );
8054 this.togglePositioning( !!this.$floatableContainer
);
8055 this.toggleClipping( true );
8057 this.bindDocumentKeyDownListener();
8058 this.bindDocumentKeyPressListener();
8061 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
8062 this.originalVerticalPosition
!== 'center'
8064 // If opening the menu in one direction causes it to be clipped, flip it
8065 originalHeight
= this.$element
.height();
8066 this.setVerticalPosition(
8067 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
8069 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
8070 // If flipping also causes it to be clipped, open in whichever direction
8071 // we have more space
8072 flippedHeight
= this.$element
.height();
8073 if ( originalHeight
> flippedHeight
) {
8074 this.setVerticalPosition( this.originalVerticalPosition
);
8078 // Note that we do not flip the menu's opening direction if the clipping changes
8079 // later (e.g. after the user scrolls), that seems like it would be annoying
8081 this.$focusOwner
.attr( 'aria-expanded', 'true' );
8083 selectedItem
= this.findSelectedItem();
8084 if ( !this.multiselect
&& selectedItem
) {
8085 // TODO: Verify if this is even needed; This is already done on highlight changes
8086 // in SelectWidget#highlightItem, so we should just need to highlight the item
8087 // we need to highlight here and not bother with attr or checking selections.
8088 this.$focusOwner
.attr( 'aria-activedescendant', selectedItem
.getElementId() );
8089 selectedItem
.scrollElementIntoView( { duration
: 0 } );
8093 if ( this.autoHide
) {
8094 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8097 this.emit( 'ready' );
8099 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
8100 this.unbindDocumentKeyDownListener();
8101 this.unbindDocumentKeyPressListener();
8102 this.$focusOwner
.attr( 'aria-expanded', 'false' );
8103 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8104 this.togglePositioning( false );
8105 this.toggleClipping( false );
8106 this.lastHighlightedItem
= null;
8114 * Scroll to the top of the menu
8116 OO
.ui
.MenuSelectWidget
.prototype.scrollToTop = function () {
8117 this.$element
.scrollTop( 0 );
8121 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
8122 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
8123 * users can interact with it.
8125 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8126 * OO.ui.DropdownInputWidget instead.
8129 * // A DropdownWidget with a menu that contains three options.
8130 * var dropDown = new OO.ui.DropdownWidget( {
8131 * label: 'Dropdown menu: Select a menu option',
8134 * new OO.ui.MenuOptionWidget( {
8138 * new OO.ui.MenuOptionWidget( {
8142 * new OO.ui.MenuOptionWidget( {
8150 * $( document.body ).append( dropDown.$element );
8152 * dropDown.getMenu().selectItemByData( 'b' );
8154 * dropDown.getMenu().findSelectedItem().getData(); // Returns 'b'.
8156 * For more information, please see the [OOUI documentation on MediaWiki] [1].
8158 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8161 * @extends OO.ui.Widget
8162 * @mixins OO.ui.mixin.IconElement
8163 * @mixins OO.ui.mixin.IndicatorElement
8164 * @mixins OO.ui.mixin.LabelElement
8165 * @mixins OO.ui.mixin.TitledElement
8166 * @mixins OO.ui.mixin.TabIndexedElement
8169 * @param {Object} [config] Configuration options
8170 * @cfg {Object} [menu] Configuration options to pass to
8171 * {@link OO.ui.MenuSelectWidget menu select widget}.
8172 * @cfg {jQuery|boolean} [$overlay] Render the menu into a separate layer. This configuration is
8173 * useful in cases where the expanded menu is larger than its containing `<div>`. The specified
8174 * overlay layer is usually on top of the containing `<div>` and has a larger area. By default,
8175 * the menu uses relative positioning. Pass 'true' to use the default overlay.
8176 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
8178 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
8179 // Configuration initialization
8180 config
= $.extend( { indicator
: 'down' }, config
);
8182 // Parent constructor
8183 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
8185 // Properties (must be set before TabIndexedElement constructor call)
8186 this.$handle
= $( '<span>' );
8187 this.$overlay
= ( config
.$overlay
=== true ?
8188 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
8190 // Mixin constructors
8191 OO
.ui
.mixin
.IconElement
.call( this, config
);
8192 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8193 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8194 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
8195 $titled
: this.$label
8197 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
8198 $tabIndexed
: this.$handle
8202 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
8204 $floatableContainer
: this.$element
8209 click
: this.onClick
.bind( this ),
8210 keydown
: this.onKeyDown
.bind( this ),
8211 // Hack? Handle type-to-search when menu is not expanded and not handling its own events.
8212 keypress
: this.menu
.onDocumentKeyPressHandler
,
8213 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
8215 this.menu
.connect( this, {
8216 select
: 'onMenuSelect',
8217 toggle
: 'onMenuToggle'
8224 'aria-readonly': 'true'
8227 .addClass( 'oo-ui-dropdownWidget-handle' )
8228 .append( this.$icon
, this.$label
, this.$indicator
)
8231 'aria-autocomplete': 'list',
8232 'aria-expanded': 'false',
8233 'aria-haspopup': 'true',
8234 'aria-owns': this.menu
.getElementId()
8237 .addClass( 'oo-ui-dropdownWidget' )
8238 .append( this.$handle
);
8239 this.$overlay
.append( this.menu
.$element
);
8244 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8245 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8246 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8247 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8248 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8249 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8256 * @return {OO.ui.MenuSelectWidget} Menu of widget
8258 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8263 * Handles menu select events.
8266 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8268 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8272 this.setLabel( null );
8276 selectedLabel
= item
.getLabel();
8278 // If the label is a DOM element, clone it, because setLabel will append() it
8279 if ( selectedLabel
instanceof $ ) {
8280 selectedLabel
= selectedLabel
.clone();
8283 this.setLabel( selectedLabel
);
8287 * Handle menu toggle events.
8290 * @param {boolean} isVisible Open state of the menu
8292 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8293 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8297 * Handle mouse click events.
8300 * @param {jQuery.Event} e Mouse click event
8301 * @return {undefined|boolean} False to prevent default if event is handled
8303 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8304 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8311 * Handle key down events.
8314 * @param {jQuery.Event} e Key down event
8315 * @return {undefined|boolean} False to prevent default if event is handled
8317 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8319 !this.isDisabled() &&
8321 e
.which
=== OO
.ui
.Keys
.ENTER
||
8323 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8324 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8325 // Space only closes the menu is the user is not typing to search.
8326 this.menu
.keyPressBuffer
=== ''
8329 !this.menu
.isVisible() &&
8331 e
.which
=== OO
.ui
.Keys
.UP
||
8332 e
.which
=== OO
.ui
.Keys
.DOWN
8343 * RadioOptionWidget is an option widget that looks like a radio button.
8344 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8345 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8347 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8350 * @extends OO.ui.OptionWidget
8353 * @param {Object} [config] Configuration options
8355 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8356 // Configuration initialization
8357 config
= config
|| {};
8359 // Properties (must be done before parent constructor which calls #setDisabled)
8360 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8362 // Parent constructor
8363 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8366 // Remove implicit role, we're handling it ourselves
8367 this.radio
.$input
.attr( 'role', 'presentation' );
8369 .addClass( 'oo-ui-radioOptionWidget' )
8370 .attr( 'role', 'radio' )
8371 .attr( 'aria-checked', 'false' )
8372 .removeAttr( 'aria-selected' )
8373 .prepend( this.radio
.$element
);
8378 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8380 /* Static Properties */
8386 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8392 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8398 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8404 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8411 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8412 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8414 this.radio
.setSelected( state
);
8416 .attr( 'aria-checked', state
.toString() )
8417 .removeAttr( 'aria-selected' );
8425 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8426 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8428 this.radio
.setDisabled( this.isDisabled() );
8434 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8435 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8436 * an interface for adding, removing and selecting options.
8437 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8439 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8440 * OO.ui.RadioSelectInputWidget instead.
8443 * // A RadioSelectWidget with RadioOptions.
8444 * var option1 = new OO.ui.RadioOptionWidget( {
8446 * label: 'Selected radio option'
8448 * option2 = new OO.ui.RadioOptionWidget( {
8450 * label: 'Unselected radio option'
8452 * radioSelect = new OO.ui.RadioSelectWidget( {
8453 * items: [ option1, option2 ]
8456 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8457 * radioSelect.selectItem( option1 );
8459 * $( document.body ).append( radioSelect.$element );
8461 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8465 * @extends OO.ui.SelectWidget
8466 * @mixins OO.ui.mixin.TabIndexedElement
8469 * @param {Object} [config] Configuration options
8471 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8472 // Parent constructor
8473 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8475 // Mixin constructors
8476 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8480 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8481 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8486 .addClass( 'oo-ui-radioSelectWidget' )
8487 .attr( 'role', 'radiogroup' );
8492 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8493 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8496 * MultioptionWidgets are special elements that can be selected and configured with data. The
8497 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8498 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8499 * and examples, please see the [OOUI documentation on MediaWiki][1].
8501 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8504 * @extends OO.ui.Widget
8505 * @mixins OO.ui.mixin.ItemWidget
8506 * @mixins OO.ui.mixin.LabelElement
8507 * @mixins OO.ui.mixin.TitledElement
8510 * @param {Object} [config] Configuration options
8511 * @cfg {boolean} [selected=false] Whether the option is initially selected
8513 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8514 // Configuration initialization
8515 config
= config
|| {};
8517 // Parent constructor
8518 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8520 // Mixin constructors
8521 OO
.ui
.mixin
.ItemWidget
.call( this );
8522 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8523 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8526 this.selected
= null;
8530 .addClass( 'oo-ui-multioptionWidget' )
8531 .append( this.$label
);
8532 this.setSelected( config
.selected
);
8537 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8538 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8539 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8540 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.TitledElement
);
8547 * A change event is emitted when the selected state of the option changes.
8549 * @param {boolean} selected Whether the option is now selected
8555 * Check if the option is selected.
8557 * @return {boolean} Item is selected
8559 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8560 return this.selected
;
8564 * Set the option’s selected state. In general, all modifications to the selection
8565 * should be handled by the SelectWidget’s
8566 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
8568 * @param {boolean} [state=false] Select option
8570 * @return {OO.ui.Widget} The widget, for chaining
8572 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8574 if ( this.selected
!== state
) {
8575 this.selected
= state
;
8576 this.emit( 'change', state
);
8577 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8583 * MultiselectWidget allows selecting multiple options from a list.
8585 * For more information about menus and options, please see the [OOUI documentation
8588 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8592 * @extends OO.ui.Widget
8593 * @mixins OO.ui.mixin.GroupWidget
8594 * @mixins OO.ui.mixin.TitledElement
8597 * @param {Object} [config] Configuration options
8598 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8600 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8601 // Parent constructor
8602 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8604 // Configuration initialization
8605 config
= config
|| {};
8607 // Mixin constructors
8608 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8609 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8615 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8616 // by GroupElement only when items are added/removed
8617 this.connect( this, {
8618 select
: [ 'emit', 'change' ]
8622 if ( config
.items
) {
8623 this.addItems( config
.items
);
8625 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8626 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8627 .append( this.$group
);
8632 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8633 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8634 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.TitledElement
);
8641 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8647 * A select event is emitted when an item is selected or deselected.
8653 * Find options that are selected.
8655 * @return {OO.ui.MultioptionWidget[]} Selected options
8657 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8658 return this.items
.filter( function ( item
) {
8659 return item
.isSelected();
8664 * Find the data of options that are selected.
8666 * @return {Object[]|string[]} Values of selected options
8668 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8669 return this.findSelectedItems().map( function ( item
) {
8675 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8677 * @param {OO.ui.MultioptionWidget[]} items Items to select
8679 * @return {OO.ui.Widget} The widget, for chaining
8681 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8682 this.items
.forEach( function ( item
) {
8683 var selected
= items
.indexOf( item
) !== -1;
8684 item
.setSelected( selected
);
8690 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8692 * @param {Object[]|string[]} datas Values of items to select
8694 * @return {OO.ui.Widget} The widget, for chaining
8696 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8699 items
= datas
.map( function ( data
) {
8700 return widget
.findItemFromData( data
);
8702 this.selectItems( items
);
8707 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8708 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8709 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8711 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8714 * @extends OO.ui.MultioptionWidget
8717 * @param {Object} [config] Configuration options
8719 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8720 // Configuration initialization
8721 config
= config
|| {};
8723 // Properties (must be done before parent constructor which calls #setDisabled)
8724 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8726 // Parent constructor
8727 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8730 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8731 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8735 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8736 .prepend( this.checkbox
.$element
);
8741 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8743 /* Static Properties */
8749 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8754 * Handle checkbox selected state change.
8758 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8759 this.setSelected( this.checkbox
.isSelected() );
8765 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8766 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8767 this.checkbox
.setSelected( state
);
8774 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8775 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8776 this.checkbox
.setDisabled( this.isDisabled() );
8783 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8784 this.checkbox
.focus();
8788 * Handle key down events.
8791 * @param {jQuery.Event} e
8793 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8795 element
= this.getElementGroup(),
8798 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8799 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8800 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8801 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8811 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8812 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8813 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8814 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8816 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8817 * OO.ui.CheckboxMultiselectInputWidget instead.
8820 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8821 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8824 * label: 'Selected checkbox'
8826 * option2 = new OO.ui.CheckboxMultioptionWidget( {
8828 * label: 'Unselected checkbox'
8830 * multiselect = new OO.ui.CheckboxMultiselectWidget( {
8831 * items: [ option1, option2 ]
8833 * $( document.body ).append( multiselect.$element );
8835 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8838 * @extends OO.ui.MultiselectWidget
8841 * @param {Object} [config] Configuration options
8843 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8844 // Parent constructor
8845 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8848 this.$lastClicked
= null;
8851 this.$group
.on( 'click', this.onClick
.bind( this ) );
8854 this.$element
.addClass( 'oo-ui-checkboxMultiselectWidget' );
8859 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8864 * Get an option by its position relative to the specified item (or to the start of the
8865 * option array, if item is `null`). The direction in which to search through the option array
8866 * is specified with a number: -1 for reverse (the default) or 1 for forward. The method will
8867 * return an option, or `null` if there are no options in the array.
8869 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or
8870 * `null` to start at the beginning of the array.
8871 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8872 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items
8875 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8876 var currentIndex
, nextIndex
, i
,
8877 increase
= direction
> 0 ? 1 : -1,
8878 len
= this.items
.length
;
8881 currentIndex
= this.items
.indexOf( item
);
8882 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8884 // If no item is selected and moving forward, start at the beginning.
8885 // If moving backward, start at the end.
8886 nextIndex
= direction
> 0 ? 0 : len
- 1;
8889 for ( i
= 0; i
< len
; i
++ ) {
8890 item
= this.items
[ nextIndex
];
8891 if ( item
&& !item
.isDisabled() ) {
8894 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8900 * Handle click events on checkboxes.
8902 * @param {jQuery.Event} e
8904 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8905 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8906 $lastClicked
= this.$lastClicked
,
8907 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8908 .not( '.oo-ui-widget-disabled' );
8910 // Allow selecting multiple options at once by Shift-clicking them
8911 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8912 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8913 lastClickedIndex
= $options
.index( $lastClicked
);
8914 nowClickedIndex
= $options
.index( $nowClicked
);
8915 // If it's the same item, either the user is being silly, or it's a fake event generated
8916 // by the browser. In either case we don't need custom handling.
8917 if ( nowClickedIndex
!== lastClickedIndex
) {
8919 wasSelected
= items
[ nowClickedIndex
].isSelected();
8920 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8922 // This depends on the DOM order of the items and the order of the .items array being
8924 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8925 if ( !items
[ i
].isDisabled() ) {
8926 items
[ i
].setSelected( !wasSelected
);
8929 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8930 // handling first, then set our value. The order in which events happen is different for
8931 // clicks on the <input> and on the <label> and there are additional fake clicks fired
8932 // for non-click actions that change the checkboxes.
8934 setTimeout( function () {
8935 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8936 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8942 if ( $nowClicked
.length
) {
8943 this.$lastClicked
= $nowClicked
;
8951 * @return {OO.ui.Widget} The widget, for chaining
8953 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8955 if ( !this.isDisabled() ) {
8956 item
= this.getRelativeFocusableItem( null, 1 );
8967 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8972 * Progress bars visually display the status of an operation, such as a download,
8973 * and can be either determinate or indeterminate:
8975 * - **determinate** process bars show the percent of an operation that is complete.
8977 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8978 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8979 * not use percentages.
8981 * The value of the `progress` configuration determines whether the bar is determinate
8985 * // Examples of determinate and indeterminate progress bars.
8986 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8989 * var progressBar2 = new OO.ui.ProgressBarWidget();
8991 * // Create a FieldsetLayout to layout progress bars.
8992 * var fieldset = new OO.ui.FieldsetLayout;
8993 * fieldset.addItems( [
8994 * new OO.ui.FieldLayout( progressBar1, {
8995 * label: 'Determinate',
8998 * new OO.ui.FieldLayout( progressBar2, {
8999 * label: 'Indeterminate',
9003 * $( document.body ).append( fieldset.$element );
9006 * @extends OO.ui.Widget
9009 * @param {Object} [config] Configuration options
9010 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
9011 * To create a determinate progress bar, specify a number that reflects the initial
9013 * By default, the progress bar is indeterminate.
9015 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
9016 // Configuration initialization
9017 config
= config
|| {};
9019 // Parent constructor
9020 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
9023 this.$bar
= $( '<div>' );
9024 this.progress
= null;
9027 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
9028 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
9031 role
: 'progressbar',
9033 'aria-valuemax': 100
9035 .addClass( 'oo-ui-progressBarWidget' )
9036 .append( this.$bar
);
9041 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
9043 /* Static Properties */
9049 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
9054 * Get the percent of the progress that has been completed. Indeterminate progresses will
9057 * @return {number|boolean} Progress percent
9059 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
9060 return this.progress
;
9064 * Set the percent of the process completed or `false` for an indeterminate process.
9066 * @param {number|boolean} progress Progress percent or `false` for indeterminate
9068 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
9069 this.progress
= progress
;
9071 if ( progress
!== false ) {
9072 this.$bar
.css( 'width', this.progress
+ '%' );
9073 this.$element
.attr( 'aria-valuenow', this.progress
);
9075 this.$bar
.css( 'width', '' );
9076 this.$element
.removeAttr( 'aria-valuenow' );
9078 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
9082 * InputWidget is the base class for all input widgets, which
9083 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox
9084 * inputs}, {@link OO.ui.RadioInputWidget radio inputs}, and
9085 * {@link OO.ui.ButtonInputWidget button inputs}.
9086 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
9088 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9092 * @extends OO.ui.Widget
9093 * @mixins OO.ui.mixin.TabIndexedElement
9094 * @mixins OO.ui.mixin.TitledElement
9095 * @mixins OO.ui.mixin.AccessKeyedElement
9098 * @param {Object} [config] Configuration options
9099 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9100 * @cfg {string} [value=''] The value of the input.
9101 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
9102 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
9103 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the
9104 * value of an input before it is accepted.
9106 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
9107 // Configuration initialization
9108 config
= config
|| {};
9110 // Parent constructor
9111 OO
.ui
.InputWidget
.parent
.call( this, config
);
9114 // See #reusePreInfuseDOM about config.$input
9115 this.$input
= config
.$input
|| this.getInputElement( config
);
9117 this.inputFilter
= config
.inputFilter
;
9119 // Mixin constructors
9120 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
9121 $tabIndexed
: this.$input
9123 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
9124 $titled
: this.$input
9126 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
9127 $accessKeyed
: this.$input
9131 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
9135 .addClass( 'oo-ui-inputWidget-input' )
9136 .attr( 'name', config
.name
)
9137 .prop( 'disabled', this.isDisabled() );
9139 .addClass( 'oo-ui-inputWidget' )
9140 .append( this.$input
);
9141 this.setValue( config
.value
);
9143 this.setDir( config
.dir
);
9145 if ( config
.inputId
!== undefined ) {
9146 this.setInputId( config
.inputId
);
9152 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
9153 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
9154 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
9155 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
9157 /* Static Methods */
9162 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9163 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9164 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
9165 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
9172 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9173 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9174 if ( config
.$input
&& config
.$input
.length
) {
9175 state
.value
= config
.$input
.val();
9176 // Might be better in TabIndexedElement, but it's awkward to do there because
9177 // mixins are awkward
9178 state
.focus
= config
.$input
.is( ':focus' );
9188 * A change event is emitted when the value of the input changes.
9190 * @param {string} value
9196 * Get input element.
9198 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9199 * different circumstances. The element must have a `value` property (like form elements).
9202 * @param {Object} config Configuration options
9203 * @return {jQuery} Input element
9205 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
9206 return $( '<input>' );
9210 * Handle potentially value-changing events.
9213 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9215 OO
.ui
.InputWidget
.prototype.onEdit = function () {
9217 if ( !this.isDisabled() ) {
9218 // Allow the stack to clear so the value will be updated
9219 setTimeout( function () {
9220 widget
.setValue( widget
.$input
.val() );
9226 * Get the value of the input.
9228 * @return {string} Input value
9230 OO
.ui
.InputWidget
.prototype.getValue = function () {
9231 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9232 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9233 var value
= this.$input
.val();
9234 if ( this.value
!== value
) {
9235 this.setValue( value
);
9241 * Set the directionality of the input.
9243 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9245 * @return {OO.ui.Widget} The widget, for chaining
9247 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9248 this.$input
.prop( 'dir', dir
);
9253 * Set the value of the input.
9255 * @param {string} value New value
9258 * @return {OO.ui.Widget} The widget, for chaining
9260 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9261 value
= this.cleanUpValue( value
);
9262 // Update the DOM if it has changed. Note that with cleanUpValue, it
9263 // is possible for the DOM value to change without this.value changing.
9264 if ( this.$input
.val() !== value
) {
9265 this.$input
.val( value
);
9267 if ( this.value
!== value
) {
9269 this.emit( 'change', this.value
);
9271 // The first time that the value is set (probably while constructing the widget),
9272 // remember it in defaultValue. This property can be later used to check whether
9273 // the value of the input has been changed since it was created.
9274 if ( this.defaultValue
=== undefined ) {
9275 this.defaultValue
= this.value
;
9276 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9282 * Clean up incoming value.
9284 * Ensures value is a string, and converts undefined and null to empty string.
9287 * @param {string} value Original value
9288 * @return {string} Cleaned up value
9290 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9291 if ( value
=== undefined || value
=== null ) {
9293 } else if ( this.inputFilter
) {
9294 return this.inputFilter( String( value
) );
9296 return String( value
);
9303 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9304 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9305 if ( this.$input
) {
9306 this.$input
.prop( 'disabled', this.isDisabled() );
9312 * Set the 'id' attribute of the `<input>` element.
9314 * @param {string} id
9316 * @return {OO.ui.Widget} The widget, for chaining
9318 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9319 this.$input
.attr( 'id', id
);
9326 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9327 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9328 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9329 this.setValue( state
.value
);
9331 if ( state
.focus
) {
9337 * Data widget intended for creating `<input type="hidden">` inputs.
9340 * @extends OO.ui.Widget
9343 * @param {Object} [config] Configuration options
9344 * @cfg {string} [value=''] The value of the input.
9345 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9347 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9348 // Configuration initialization
9349 config
= $.extend( { value
: '', name
: '' }, config
);
9351 // Parent constructor
9352 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9355 this.$element
.attr( {
9357 value
: config
.value
,
9360 this.$element
.removeAttr( 'aria-disabled' );
9365 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9367 /* Static Properties */
9373 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9376 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9377 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9378 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9379 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9380 * [OOUI documentation on MediaWiki] [1] for more information.
9383 * // A ButtonInputWidget rendered as an HTML button, the default.
9384 * var button = new OO.ui.ButtonInputWidget( {
9385 * label: 'Input button',
9389 * $( document.body ).append( button.$element );
9391 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9394 * @extends OO.ui.InputWidget
9395 * @mixins OO.ui.mixin.ButtonElement
9396 * @mixins OO.ui.mixin.IconElement
9397 * @mixins OO.ui.mixin.IndicatorElement
9398 * @mixins OO.ui.mixin.LabelElement
9399 * @mixins OO.ui.mixin.FlaggedElement
9402 * @param {Object} [config] Configuration options
9403 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute:
9404 * 'button', 'submit' or 'reset'.
9405 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9406 * Widgets configured to be an `<input>` do not support {@link #icon icons} and
9407 * {@link #indicator indicators},
9408 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should
9409 * only be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9411 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9412 // Configuration initialization
9413 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9415 // See InputWidget#reusePreInfuseDOM about config.$input
9416 if ( config
.$input
) {
9417 config
.$input
.empty();
9420 // Properties (must be set before parent constructor, which calls #setValue)
9421 this.useInputTag
= config
.useInputTag
;
9423 // Parent constructor
9424 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9426 // Mixin constructors
9427 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {
9428 $button
: this.$input
9430 OO
.ui
.mixin
.IconElement
.call( this, config
);
9431 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9432 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9433 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
9436 if ( !config
.useInputTag
) {
9437 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9439 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9444 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9445 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9446 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9447 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9448 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9449 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
9451 /* Static Properties */
9457 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9465 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9467 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9468 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9474 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9476 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9477 * text, or `null` for no label
9479 * @return {OO.ui.Widget} The widget, for chaining
9481 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9482 if ( typeof label
=== 'function' ) {
9483 label
= OO
.ui
.resolveMsg( label
);
9486 if ( this.useInputTag
) {
9487 // Discard non-plaintext labels
9488 if ( typeof label
!== 'string' ) {
9492 this.$input
.val( label
);
9495 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9499 * Set the value of the input.
9501 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9502 * they do not support {@link #value values}.
9504 * @param {string} value New value
9506 * @return {OO.ui.Widget} The widget, for chaining
9508 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9509 if ( !this.useInputTag
) {
9510 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9518 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9519 // Disable generating `<label>` elements for buttons. One would very rarely need additional
9520 // label for a button, and it's already a big clickable target, and it causes
9521 // unexpected rendering.
9526 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9527 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9528 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9529 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9531 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9534 * // An example of selected, unselected, and disabled checkbox inputs.
9535 * var checkbox1 = new OO.ui.CheckboxInputWidget( {
9539 * checkbox2 = new OO.ui.CheckboxInputWidget( {
9542 * checkbox3 = new OO.ui.CheckboxInputWidget( {
9546 * // Create a fieldset layout with fields for each checkbox.
9547 * fieldset = new OO.ui.FieldsetLayout( {
9548 * label: 'Checkboxes'
9550 * fieldset.addItems( [
9551 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9552 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9553 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9555 * $( document.body ).append( fieldset.$element );
9557 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9560 * @extends OO.ui.InputWidget
9563 * @param {Object} [config] Configuration options
9564 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is
9566 * @cfg {boolean} [indeterminate=false] Whether the checkbox is in the indeterminate state.
9568 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9569 // Configuration initialization
9570 config
= config
|| {};
9572 // Parent constructor
9573 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9576 this.checkIcon
= new OO
.ui
.IconWidget( {
9578 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9583 .addClass( 'oo-ui-checkboxInputWidget' )
9584 // Required for pretty styling in WikimediaUI theme
9585 .append( this.checkIcon
.$element
);
9586 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9587 this.setIndeterminate( config
.indeterminate
!== undefined ? config
.indeterminate
: false );
9592 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9599 * A change event is emitted when the state of the input changes.
9601 * @param {boolean} selected
9602 * @param {boolean} indeterminate
9605 /* Static Properties */
9611 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9613 /* Static Methods */
9618 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9619 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9620 state
.checked
= config
.$input
.prop( 'checked' );
9630 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9631 return $( '<input>' ).attr( 'type', 'checkbox' );
9637 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9639 if ( !this.isDisabled() ) {
9640 // Allow the stack to clear so the value will be updated
9641 setTimeout( function () {
9642 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9643 widget
.setIndeterminate( widget
.$input
.prop( 'indeterminate' ) );
9649 * Set selection state of this checkbox.
9651 * @param {boolean} state Selected state
9652 * @param {boolean} internal Used for internal calls to suppress events
9654 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9656 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
, internal ) {
9658 if ( this.selected
!== state
) {
9659 this.selected
= state
;
9660 this.$input
.prop( 'checked', this.selected
);
9662 this.setIndeterminate( false, true );
9663 this.emit( 'change', this.selected
, this.indeterminate
);
9666 // The first time that the selection state is set (probably while constructing the widget),
9667 // remember it in defaultSelected. This property can be later used to check whether
9668 // the selection state of the input has been changed since it was created.
9669 if ( this.defaultSelected
=== undefined ) {
9670 this.defaultSelected
= this.selected
;
9671 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9677 * Check if this checkbox is selected.
9679 * @return {boolean} Checkbox is selected
9681 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9682 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9683 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9684 var selected
= this.$input
.prop( 'checked' );
9685 if ( this.selected
!== selected
) {
9686 this.setSelected( selected
);
9688 return this.selected
;
9692 * Set indeterminate state of this checkbox.
9694 * @param {boolean} state Indeterminate state
9695 * @param {boolean} internal Used for internal calls to suppress events
9697 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9699 OO
.ui
.CheckboxInputWidget
.prototype.setIndeterminate = function ( state
, internal ) {
9701 if ( this.indeterminate
!== state
) {
9702 this.indeterminate
= state
;
9703 this.$input
.prop( 'indeterminate', this.indeterminate
);
9705 this.setSelected( false, true );
9706 this.emit( 'change', this.selected
, this.indeterminate
);
9713 * Check if this checkbox is selected.
9715 * @return {boolean} Checkbox is selected
9717 OO
.ui
.CheckboxInputWidget
.prototype.isIndeterminate = function () {
9718 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9719 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9720 var indeterminate
= this.$input
.prop( 'indeterminate' );
9721 if ( this.indeterminate
!== indeterminate
) {
9722 this.setIndeterminate( indeterminate
);
9724 return this.indeterminate
;
9730 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9731 if ( !this.isDisabled() ) {
9732 this.$handle
.trigger( 'click' );
9740 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9741 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9742 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9743 this.setSelected( state
.checked
);
9748 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9749 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the
9750 * value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9751 * more information about input widgets.
9753 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9754 * are no options. If no `value` configuration option is provided, the first option is selected.
9755 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9757 * This and OO.ui.RadioSelectInputWidget support similar configuration options.
9760 * // A DropdownInputWidget with three options.
9761 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9763 * { data: 'a', label: 'First' },
9764 * { data: 'b', label: 'Second', disabled: true },
9765 * { optgroup: 'Group label' },
9766 * { data: 'c', label: 'First sub-item)' }
9769 * $( document.body ).append( dropdownInput.$element );
9771 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9774 * @extends OO.ui.InputWidget
9777 * @param {Object} [config] Configuration options
9778 * @cfg {Object[]} [options=[]] Array of menu options in the format described above.
9779 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9780 * @cfg {jQuery|boolean} [$overlay] Render the menu into a separate layer. This configuration is
9781 * useful in cases where the expanded menu is larger than its containing `<div>`. The specified
9782 * overlay layer is usually on top of the containing `<div>` and has a larger area. By default,
9783 * the menu uses relative positioning. Pass 'true' to use the default overlay.
9784 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9786 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9787 // Configuration initialization
9788 config
= config
|| {};
9790 // Properties (must be done before parent constructor which calls #setDisabled)
9791 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9793 $overlay
: config
.$overlay
9797 // Set up the options before parent constructor, which uses them to validate config.value.
9798 // Use this instead of setOptions() because this.$input is not set up yet.
9799 this.setOptionsData( config
.options
|| [] );
9801 // Parent constructor
9802 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9805 this.dropdownWidget
.getMenu().connect( this, {
9806 select
: 'onMenuSelect'
9811 .addClass( 'oo-ui-dropdownInputWidget' )
9812 .append( this.dropdownWidget
.$element
);
9813 if ( OO
.ui
.isMobile() ) {
9814 this.$element
.addClass( 'oo-ui-isMobile' );
9816 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9817 this.setTitledElement( this.dropdownWidget
.$handle
);
9822 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9830 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9831 return $( '<select>' ).addClass( 'oo-ui-indicator-down' );
9835 * Handles menu select events.
9838 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9840 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9841 this.setValue( item
? item
.getData() : '' );
9847 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9849 value
= this.cleanUpValue( value
);
9850 // Only allow setting values that are actually present in the dropdown
9851 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9852 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9853 this.dropdownWidget
.getMenu().selectItem( selected
);
9854 value
= selected
? selected
.getData() : '';
9855 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9856 if ( this.optionsDirty
) {
9857 // We reached this from the constructor or from #setOptions.
9858 // We have to update the <select> element.
9859 this.updateOptionsInterface();
9867 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9868 this.dropdownWidget
.setDisabled( state
);
9869 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9874 * Set the options available for this input.
9876 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9878 * @return {OO.ui.Widget} The widget, for chaining
9880 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9881 var value
= this.getValue();
9883 this.setOptionsData( options
);
9885 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9886 // In case the previous value is no longer an available option, select the first valid one.
9887 this.setValue( value
);
9893 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9895 * This method may be called before the parent constructor, so various properties may not be
9898 * @param {Object[]} options Array of menu options (see #constructor for details).
9901 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9902 var optionWidgets
, optIndex
, opt
, previousOptgroup
, optionWidget
, optValue
,
9905 this.optionsDirty
= true;
9907 // Go through all the supplied option configs and create either
9908 // MenuSectionOption or MenuOption widgets from each.
9910 for ( optIndex
= 0; optIndex
< options
.length
; optIndex
++ ) {
9911 opt
= options
[ optIndex
];
9913 if ( opt
.optgroup
!== undefined ) {
9914 // Create a <optgroup> menu item.
9915 optionWidget
= widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9916 previousOptgroup
= optionWidget
;
9919 // Create a normal <option> menu item.
9920 optValue
= widget
.cleanUpValue( opt
.data
);
9921 optionWidget
= widget
.createMenuOptionWidget(
9923 opt
.label
!== undefined ? opt
.label
: optValue
9927 // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
9929 opt
.disabled
!== undefined ||
9930 previousOptgroup
instanceof OO
.ui
.MenuSectionOptionWidget
&&
9931 previousOptgroup
.isDisabled()
9933 optionWidget
.setDisabled( true );
9936 optionWidgets
.push( optionWidget
);
9939 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9943 * Create a menu option widget.
9946 * @param {string} data Item data
9947 * @param {string} label Item label
9948 * @return {OO.ui.MenuOptionWidget} Option widget
9950 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9951 return new OO
.ui
.MenuOptionWidget( {
9958 * Create a menu section option widget.
9961 * @param {string} label Section item label
9962 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9964 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9965 return new OO
.ui
.MenuSectionOptionWidget( {
9971 * Update the user-visible interface to match the internal list of options and value.
9973 * This method must only be called after the parent constructor.
9977 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9979 $optionsContainer
= this.$input
,
9980 defaultValue
= this.defaultValue
,
9983 this.$input
.empty();
9985 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9988 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9989 $optionNode
= $( '<option>' )
9990 .attr( 'value', optionWidget
.getData() )
9991 .text( optionWidget
.getLabel() );
9993 // Remember original selection state. This property can be later used to check whether
9994 // the selection state of the input has been changed since it was created.
9995 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9997 $optionsContainer
.append( $optionNode
);
9999 $optionNode
= $( '<optgroup>' )
10000 .attr( 'label', optionWidget
.getLabel() );
10001 widget
.$input
.append( $optionNode
);
10002 $optionsContainer
= $optionNode
;
10005 // Disable the option or optgroup if required.
10006 if ( optionWidget
.isDisabled() ) {
10007 $optionNode
.prop( 'disabled', true );
10011 this.optionsDirty
= false;
10017 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
10018 this.dropdownWidget
.focus();
10025 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
10026 this.dropdownWidget
.blur();
10031 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
10032 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
10033 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
10034 * please see the [OOUI documentation on MediaWiki][1].
10036 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10039 * // An example of selected, unselected, and disabled radio inputs
10040 * var radio1 = new OO.ui.RadioInputWidget( {
10044 * var radio2 = new OO.ui.RadioInputWidget( {
10047 * var radio3 = new OO.ui.RadioInputWidget( {
10051 * // Create a fieldset layout with fields for each radio button.
10052 * var fieldset = new OO.ui.FieldsetLayout( {
10053 * label: 'Radio inputs'
10055 * fieldset.addItems( [
10056 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
10057 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
10058 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
10060 * $( document.body ).append( fieldset.$element );
10062 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10065 * @extends OO.ui.InputWidget
10068 * @param {Object} [config] Configuration options
10069 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button
10072 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
10073 // Configuration initialization
10074 config
= config
|| {};
10076 // Parent constructor
10077 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
10081 .addClass( 'oo-ui-radioInputWidget' )
10082 // Required for pretty styling in WikimediaUI theme
10083 .append( $( '<span>' ) );
10084 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
10089 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
10091 /* Static Properties */
10097 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
10099 /* Static Methods */
10104 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10105 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10106 state
.checked
= config
.$input
.prop( 'checked' );
10116 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
10117 return $( '<input>' ).attr( 'type', 'radio' );
10123 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
10124 // RadioInputWidget doesn't track its state.
10128 * Set selection state of this radio button.
10130 * @param {boolean} state `true` for selected
10132 * @return {OO.ui.Widget} The widget, for chaining
10134 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
10135 // RadioInputWidget doesn't track its state.
10136 this.$input
.prop( 'checked', state
);
10137 // The first time that the selection state is set (probably while constructing the widget),
10138 // remember it in defaultSelected. This property can be later used to check whether
10139 // the selection state of the input has been changed since it was created.
10140 if ( this.defaultSelected
=== undefined ) {
10141 this.defaultSelected
= state
;
10142 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
10148 * Check if this radio button is selected.
10150 * @return {boolean} Radio is selected
10152 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
10153 return this.$input
.prop( 'checked' );
10159 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
10160 if ( !this.isDisabled() ) {
10161 this.$input
.trigger( 'click' );
10169 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10170 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10171 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
10172 this.setSelected( state
.checked
);
10177 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be
10178 * used within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with
10179 * the value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
10180 * more information about input widgets.
10182 * This and OO.ui.DropdownInputWidget support similar configuration options.
10185 * // A RadioSelectInputWidget with three options
10186 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
10188 * { data: 'a', label: 'First' },
10189 * { data: 'b', label: 'Second'},
10190 * { data: 'c', label: 'Third' }
10193 * $( document.body ).append( radioSelectInput.$element );
10195 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10198 * @extends OO.ui.InputWidget
10201 * @param {Object} [config] Configuration options
10202 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10204 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
10205 // Configuration initialization
10206 config
= config
|| {};
10208 // Properties (must be done before parent constructor which calls #setDisabled)
10209 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
10210 // Set up the options before parent constructor, which uses them to validate config.value.
10211 // Use this instead of setOptions() because this.$input is not set up yet
10212 this.setOptionsData( config
.options
|| [] );
10214 // Parent constructor
10215 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
10218 this.radioSelectWidget
.connect( this, {
10219 select
: 'onMenuSelect'
10224 .addClass( 'oo-ui-radioSelectInputWidget' )
10225 .append( this.radioSelectWidget
.$element
);
10226 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
10231 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
10233 /* Static Methods */
10238 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10239 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10240 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
10247 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10248 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10249 // Cannot reuse the `<input type=radio>` set
10250 delete config
.$input
;
10260 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
10261 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
10262 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
10263 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
10267 * Handles menu select events.
10270 * @param {OO.ui.RadioOptionWidget} item Selected menu item
10272 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
10273 this.setValue( item
.getData() );
10279 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
10281 value
= this.cleanUpValue( value
);
10282 // Only allow setting values that are actually present in the dropdown
10283 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
10284 this.radioSelectWidget
.findFirstSelectableItem();
10285 this.radioSelectWidget
.selectItem( selected
);
10286 value
= selected
? selected
.getData() : '';
10287 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10294 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10295 this.radioSelectWidget
.setDisabled( state
);
10296 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10301 * Set the options available for this input.
10303 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10305 * @return {OO.ui.Widget} The widget, for chaining
10307 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10308 var value
= this.getValue();
10310 this.setOptionsData( options
);
10312 // Re-set the value to update the visible interface (RadioSelectWidget).
10313 // In case the previous value is no longer an available option, select the first valid one.
10314 this.setValue( value
);
10320 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10322 * This method may be called before the parent constructor, so various properties may not be
10325 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10328 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10331 this.radioSelectWidget
10333 .addItems( options
.map( function ( opt
) {
10334 var optValue
= widget
.cleanUpValue( opt
.data
);
10335 return new OO
.ui
.RadioOptionWidget( {
10337 label
: opt
.label
!== undefined ? opt
.label
: optValue
10345 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10346 this.radioSelectWidget
.focus();
10353 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10354 this.radioSelectWidget
.blur();
10359 * CheckboxMultiselectInputWidget is a
10360 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10361 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10362 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10363 * more information about input widgets.
10366 * // A CheckboxMultiselectInputWidget with three options.
10367 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10369 * { data: 'a', label: 'First' },
10370 * { data: 'b', label: 'Second' },
10371 * { data: 'c', label: 'Third' }
10374 * $( document.body ).append( multiselectInput.$element );
10376 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10379 * @extends OO.ui.InputWidget
10382 * @param {Object} [config] Configuration options
10383 * @cfg {Object[]} [options=[]] Array of menu options in the format
10384 * `{ data: …, label: …, disabled: … }`
10386 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10387 // Configuration initialization
10388 config
= config
|| {};
10390 // Properties (must be done before parent constructor which calls #setDisabled)
10391 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10392 // Must be set before the #setOptionsData call below
10393 this.inputName
= config
.name
;
10394 // Set up the options before parent constructor, which uses them to validate config.value.
10395 // Use this instead of setOptions() because this.$input is not set up yet
10396 this.setOptionsData( config
.options
|| [] );
10398 // Parent constructor
10399 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10402 this.checkboxMultiselectWidget
.connect( this, {
10403 select
: 'onCheckboxesSelect'
10408 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10409 .append( this.checkboxMultiselectWidget
.$element
);
10410 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10411 this.$input
.detach();
10416 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10418 /* Static Methods */
10423 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10424 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState(
10427 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10428 .toArray().map( function ( el
) { return el
.value
; } );
10435 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10436 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10437 // Cannot reuse the `<input type=checkbox>` set
10438 delete config
.$input
;
10448 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10450 return $( '<unused>' );
10454 * Handles CheckboxMultiselectWidget select events.
10458 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10459 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10465 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10466 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10467 .toArray().map( function ( el
) { return el
.value
; } );
10468 if ( this.value
!== value
) {
10469 this.setValue( value
);
10477 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10478 value
= this.cleanUpValue( value
);
10479 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10480 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10481 if ( this.optionsDirty
) {
10482 // We reached this from the constructor or from #setOptions.
10483 // We have to update the <select> element.
10484 this.updateOptionsInterface();
10490 * Clean up incoming value.
10492 * @param {string[]} value Original value
10493 * @return {string[]} Cleaned up value
10495 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10496 var i
, singleValue
,
10498 if ( !Array
.isArray( value
) ) {
10501 for ( i
= 0; i
< value
.length
; i
++ ) {
10502 singleValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10503 .call( this, value
[ i
] );
10504 // Remove options that we don't have here
10505 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10508 cleanValue
.push( singleValue
);
10516 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10517 this.checkboxMultiselectWidget
.setDisabled( state
);
10518 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10523 * Set the options available for this input.
10525 * @param {Object[]} options Array of menu options in the format
10526 * `{ data: …, label: …, disabled: … }`
10528 * @return {OO.ui.Widget} The widget, for chaining
10530 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10531 var value
= this.getValue();
10533 this.setOptionsData( options
);
10535 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10536 // This will also get rid of any stale options that we just removed.
10537 this.setValue( value
);
10543 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10545 * This method may be called before the parent constructor, so various properties may not be
10548 * @param {Object[]} options Array of menu options in the format
10549 * `{ data: …, label: … }`
10552 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10555 this.optionsDirty
= true;
10557 this.checkboxMultiselectWidget
10559 .addItems( options
.map( function ( opt
) {
10560 var optValue
, item
, optDisabled
;
10561 optValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10562 .call( widget
, opt
.data
);
10563 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10564 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10566 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10567 disabled
: optDisabled
10569 // Set the 'name' and 'value' for form submission
10570 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10571 item
.checkbox
.setValue( optValue
);
10577 * Update the user-visible interface to match the internal list of options and value.
10579 * This method must only be called after the parent constructor.
10583 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10584 var defaultValue
= this.defaultValue
;
10586 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10587 // Remember original selection state. This property can be later used to check whether
10588 // the selection state of the input has been changed since it was created.
10589 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10590 item
.checkbox
.defaultSelected
= isDefault
;
10591 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10594 this.optionsDirty
= false;
10600 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10601 this.checkboxMultiselectWidget
.focus();
10606 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10607 * size of the field as well as its presentation. In addition, these widgets can be configured
10608 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an
10609 * optional validation-pattern (used to determine if an input value is valid or not) and an input
10610 * filter, which modifies incoming values rather than validating them.
10611 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10613 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10616 * // A TextInputWidget.
10617 * var textInput = new OO.ui.TextInputWidget( {
10618 * value: 'Text input'
10620 * $( document.body ).append( textInput.$element );
10622 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10625 * @extends OO.ui.InputWidget
10626 * @mixins OO.ui.mixin.IconElement
10627 * @mixins OO.ui.mixin.IndicatorElement
10628 * @mixins OO.ui.mixin.PendingElement
10629 * @mixins OO.ui.mixin.LabelElement
10630 * @mixins OO.ui.mixin.FlaggedElement
10633 * @param {Object} [config] Configuration options
10634 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10635 * 'email', 'url' or 'number'.
10636 * @cfg {string} [placeholder] Placeholder text
10637 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10638 * instruct the browser to focus this widget.
10639 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10640 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10642 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10643 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10644 * many emojis) count as 2 characters each.
10645 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10646 * the value or placeholder text: `'before'` or `'after'`
10647 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator:
10648 * 'required'`. Note that `false` & setting `indicator: 'required' will result in no indicator
10650 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10651 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined`
10652 * means leaving it up to the browser).
10653 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10654 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10655 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10656 * value for it to be considered valid; when Function, a function receiving the value as parameter
10657 * that must return true, or promise resolving to true, for it to be considered valid.
10659 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10660 // Configuration initialization
10661 config
= $.extend( {
10663 labelPosition
: 'after'
10666 // Parent constructor
10667 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10669 // Mixin constructors
10670 OO
.ui
.mixin
.IconElement
.call( this, config
);
10671 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10672 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( { $pending
: this.$input
}, config
) );
10673 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10674 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
10677 this.type
= this.getSaneType( config
);
10678 this.readOnly
= false;
10679 this.required
= false;
10680 this.validate
= null;
10681 this.scrollWidth
= null;
10683 this.setValidation( config
.validate
);
10684 this.setLabelPosition( config
.labelPosition
);
10688 keypress
: this.onKeyPress
.bind( this ),
10689 blur
: this.onBlur
.bind( this ),
10690 focus
: this.onFocus
.bind( this )
10692 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10693 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10694 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10695 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10699 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10700 .append( this.$icon
, this.$indicator
);
10701 this.setReadOnly( !!config
.readOnly
);
10702 this.setRequired( !!config
.required
);
10703 if ( config
.placeholder
!== undefined ) {
10704 this.$input
.attr( 'placeholder', config
.placeholder
);
10706 if ( config
.maxLength
!== undefined ) {
10707 this.$input
.attr( 'maxlength', config
.maxLength
);
10709 if ( config
.autofocus
) {
10710 this.$input
.attr( 'autofocus', 'autofocus' );
10712 if ( config
.autocomplete
=== false ) {
10713 this.$input
.attr( 'autocomplete', 'off' );
10714 // Turning off autocompletion also disables "form caching" when the user navigates to a
10715 // different page and then clicks "Back". Re-enable it when leaving.
10716 // Borrowed from jQuery UI.
10718 beforeunload: function () {
10719 this.$input
.removeAttr( 'autocomplete' );
10721 pageshow: function () {
10722 // Browsers don't seem to actually fire this event on "Back", they instead just
10723 // reload the whole page... it shouldn't hurt, though.
10724 this.$input
.attr( 'autocomplete', 'off' );
10728 if ( config
.spellcheck
!== undefined ) {
10729 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10731 if ( this.label
) {
10732 this.isWaitingToBeAttached
= true;
10733 this.installParentChangeDetector();
10739 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10740 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10741 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10742 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10743 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10744 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
10746 /* Static Properties */
10748 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10756 * An `enter` event is emitted when the user presses Enter key inside the text box.
10764 * Handle icon mouse down events.
10767 * @param {jQuery.Event} e Mouse down event
10768 * @return {undefined|boolean} False to prevent default if event is handled
10770 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10771 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10778 * Handle indicator mouse down events.
10781 * @param {jQuery.Event} e Mouse down event
10782 * @return {undefined|boolean} False to prevent default if event is handled
10784 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10785 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10792 * Handle key press events.
10795 * @param {jQuery.Event} e Key press event
10796 * @fires enter If Enter key is pressed
10798 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10799 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10800 this.emit( 'enter', e
);
10805 * Handle blur events.
10808 * @param {jQuery.Event} e Blur event
10810 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10811 this.setValidityFlag();
10815 * Handle focus events.
10818 * @param {jQuery.Event} e Focus event
10820 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10821 if ( this.isWaitingToBeAttached
) {
10822 // If we've received focus, then we must be attached to the document, and if
10823 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10824 this.onElementAttach();
10826 this.setValidityFlag( true );
10830 * Handle element attach events.
10833 * @param {jQuery.Event} e Element attach event
10835 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10836 this.isWaitingToBeAttached
= false;
10837 // Any previously calculated size is now probably invalid if we reattached elsewhere
10838 this.valCache
= null;
10839 this.positionLabel();
10843 * Handle debounced change events.
10845 * @param {string} value
10848 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10849 this.setValidityFlag();
10853 * Check if the input is {@link #readOnly read-only}.
10855 * @return {boolean}
10857 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10858 return this.readOnly
;
10862 * Set the {@link #readOnly read-only} state of the input.
10864 * @param {boolean} state Make input read-only
10866 * @return {OO.ui.Widget} The widget, for chaining
10868 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10869 this.readOnly
= !!state
;
10870 this.$input
.prop( 'readOnly', this.readOnly
);
10875 * Check if the input is {@link #required required}.
10877 * @return {boolean}
10879 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10880 return this.required
;
10884 * Set the {@link #required required} state of the input.
10886 * @param {boolean} state Make input required
10888 * @return {OO.ui.Widget} The widget, for chaining
10890 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10891 this.required
= !!state
;
10892 if ( this.required
) {
10894 .prop( 'required', true )
10895 .attr( 'aria-required', 'true' );
10896 if ( this.getIndicator() === null ) {
10897 this.setIndicator( 'required' );
10901 .prop( 'required', false )
10902 .removeAttr( 'aria-required' );
10903 if ( this.getIndicator() === 'required' ) {
10904 this.setIndicator( null );
10911 * Support function for making #onElementAttach work across browsers.
10913 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10914 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10916 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10917 * first time that the element gets attached to the documented.
10919 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10920 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10921 MutationObserver
= window
.MutationObserver
||
10922 window
.WebKitMutationObserver
||
10923 window
.MozMutationObserver
,
10926 if ( MutationObserver
) {
10927 // The new way. If only it wasn't so ugly.
10929 if ( this.isElementAttached() ) {
10930 // Widget is attached already, do nothing. This breaks the functionality of this
10931 // function when the widget is detached and reattached. Alas, doing this correctly with
10932 // MutationObserver would require observation of the whole document, which would hurt
10933 // performance of other, more important code.
10937 // Find topmost node in the tree
10938 topmostNode
= this.$element
[ 0 ];
10939 while ( topmostNode
.parentNode
) {
10940 topmostNode
= topmostNode
.parentNode
;
10943 // We have no way to detect the $element being attached somewhere without observing the
10944 // entire DOM with subtree modifications, which would hurt performance. So we cheat: we hook
10945 // to the parent node of $element, and instead detect when $element is removed from it (and
10946 // thus probably attached somewhere else). If there is no parent, we create a "fake" one. If
10947 // it doesn't get attached, we end up back here and create the parent.
10948 mutationObserver
= new MutationObserver( function ( mutations
) {
10949 var i
, j
, removedNodes
;
10950 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10951 removedNodes
= mutations
[ i
].removedNodes
;
10952 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10953 if ( removedNodes
[ j
] === topmostNode
) {
10954 setTimeout( onRemove
, 0 );
10961 onRemove = function () {
10962 // If the node was attached somewhere else, report it
10963 if ( widget
.isElementAttached() ) {
10964 widget
.onElementAttach();
10966 mutationObserver
.disconnect();
10967 widget
.installParentChangeDetector();
10970 // Create a fake parent and observe it
10971 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10972 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10974 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10975 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10976 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10984 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10985 if ( this.getSaneType( config
) === 'number' ) {
10986 return $( '<input>' )
10987 .attr( 'step', 'any' )
10988 .attr( 'type', 'number' );
10990 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10995 * Get sanitized value for 'type' for given config.
10997 * @param {Object} config Configuration options
10998 * @return {string|null}
11001 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
11002 var allowedTypes
= [
11009 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
11013 * Focus the input and select a specified range within the text.
11015 * @param {number} from Select from offset
11016 * @param {number} [to] Select to offset, defaults to from
11018 * @return {OO.ui.Widget} The widget, for chaining
11020 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
11021 var isBackwards
, start
, end
,
11022 input
= this.$input
[ 0 ];
11026 isBackwards
= to
< from;
11027 start
= isBackwards
? to
: from;
11028 end
= isBackwards
? from : to
;
11033 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
11035 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
11036 // Rather than expensively check if the input is attached every time, just check
11037 // if it was the cause of an error being thrown. If not, rethrow the error.
11038 if ( this.getElementDocument().body
.contains( input
) ) {
11046 * Get an object describing the current selection range in a directional manner
11048 * @return {Object} Object containing 'from' and 'to' offsets
11050 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
11051 var input
= this.$input
[ 0 ],
11052 start
= input
.selectionStart
,
11053 end
= input
.selectionEnd
,
11054 isBackwards
= input
.selectionDirection
=== 'backward';
11057 from: isBackwards
? end
: start
,
11058 to
: isBackwards
? start
: end
11063 * Get the length of the text input value.
11065 * This could differ from the length of #getValue if the
11066 * value gets filtered
11068 * @return {number} Input length
11070 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
11071 return this.$input
[ 0 ].value
.length
;
11075 * Focus the input and select the entire text.
11078 * @return {OO.ui.Widget} The widget, for chaining
11080 OO
.ui
.TextInputWidget
.prototype.select = function () {
11081 return this.selectRange( 0, this.getInputLength() );
11085 * Focus the input and move the cursor to the start.
11088 * @return {OO.ui.Widget} The widget, for chaining
11090 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
11091 return this.selectRange( 0 );
11095 * Focus the input and move the cursor to the end.
11098 * @return {OO.ui.Widget} The widget, for chaining
11100 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
11101 return this.selectRange( this.getInputLength() );
11105 * Insert new content into the input.
11107 * @param {string} content Content to be inserted
11109 * @return {OO.ui.Widget} The widget, for chaining
11111 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
11113 range
= this.getRange(),
11114 value
= this.getValue();
11116 start
= Math
.min( range
.from, range
.to
);
11117 end
= Math
.max( range
.from, range
.to
);
11119 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
11120 this.selectRange( start
+ content
.length
);
11125 * Insert new content either side of a selection.
11127 * @param {string} pre Content to be inserted before the selection
11128 * @param {string} post Content to be inserted after the selection
11130 * @return {OO.ui.Widget} The widget, for chaining
11132 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
11134 range
= this.getRange(),
11135 offset
= pre
.length
;
11137 start
= Math
.min( range
.from, range
.to
);
11138 end
= Math
.max( range
.from, range
.to
);
11140 this.selectRange( start
).insertContent( pre
);
11141 this.selectRange( offset
+ end
).insertContent( post
);
11143 this.selectRange( offset
+ start
, offset
+ end
);
11148 * Set the validation pattern.
11150 * The validation pattern is either a regular expression, a function, or the symbolic name of a
11151 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11152 * value must contain only numbers).
11154 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
11155 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11157 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
11158 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
11159 this.validate
= validate
;
11161 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
11166 * Sets the 'invalid' flag appropriately.
11168 * @param {boolean} [isValid] Optionally override validation result
11170 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
11172 setFlag = function ( valid
) {
11174 widget
.$input
.attr( 'aria-invalid', 'true' );
11176 widget
.$input
.removeAttr( 'aria-invalid' );
11178 widget
.setFlags( { invalid
: !valid
} );
11181 if ( isValid
!== undefined ) {
11182 setFlag( isValid
);
11184 this.getValidity().then( function () {
11193 * Get the validity of current value.
11195 * This method returns a promise that resolves if the value is valid and rejects if
11196 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
11198 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
11200 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
11203 function rejectOrResolve( valid
) {
11205 return $.Deferred().resolve().promise();
11207 return $.Deferred().reject().promise();
11211 // Check browser validity and reject if it is invalid
11213 this.$input
[ 0 ].checkValidity
!== undefined &&
11214 this.$input
[ 0 ].checkValidity() === false
11216 return rejectOrResolve( false );
11219 // Run our checks if the browser thinks the field is valid
11220 if ( this.validate
instanceof Function
) {
11221 result
= this.validate( this.getValue() );
11222 if ( result
&& typeof result
.promise
=== 'function' ) {
11223 return result
.promise().then( function ( valid
) {
11224 return rejectOrResolve( valid
);
11227 return rejectOrResolve( result
);
11230 return rejectOrResolve( this.getValue().match( this.validate
) );
11235 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11237 * @param {string} labelPosition Label position, 'before' or 'after'
11239 * @return {OO.ui.Widget} The widget, for chaining
11241 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
11242 this.labelPosition
= labelPosition
;
11243 if ( this.label
) {
11244 // If there is no label and we only change the position, #updatePosition is a no-op,
11245 // but it takes really a lot of work to do nothing.
11246 this.updatePosition();
11252 * Update the position of the inline label.
11254 * This method is called by #setLabelPosition, and can also be called on its own if
11255 * something causes the label to be mispositioned.
11258 * @return {OO.ui.Widget} The widget, for chaining
11260 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
11261 var after
= this.labelPosition
=== 'after';
11264 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
11265 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
11267 this.valCache
= null;
11268 this.scrollWidth
= null;
11269 this.positionLabel();
11275 * Position the label by setting the correct padding on the input.
11279 * @return {OO.ui.Widget} The widget, for chaining
11281 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11282 var after
, rtl
, property
, newCss
;
11284 if ( this.isWaitingToBeAttached
) {
11285 // #onElementAttach will be called soon, which calls this method
11290 'padding-right': '',
11294 if ( this.label
) {
11295 this.$element
.append( this.$label
);
11297 this.$label
.detach();
11298 // Clear old values if present
11299 this.$input
.css( newCss
);
11303 after
= this.labelPosition
=== 'after';
11304 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11305 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11307 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11308 // We have to clear the padding on the other side, in case the element direction changed
11309 this.$input
.css( newCss
);
11315 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11316 * {@link OO.ui.mixin.IconElement search icon} by default.
11317 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11319 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11322 * @extends OO.ui.TextInputWidget
11325 * @param {Object} [config] Configuration options
11327 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11328 config
= $.extend( {
11332 // Parent constructor
11333 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11336 this.connect( this, {
11339 this.$indicator
.on( 'click', this.onIndicatorClick
.bind( this ) );
11342 this.updateSearchIndicator();
11343 this.connect( this, {
11344 disable
: 'onDisable'
11350 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11358 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11363 * Handle click events on the indicator
11365 * @param {jQuery.Event} e Click event
11366 * @return {boolean}
11368 OO
.ui
.SearchInputWidget
.prototype.onIndicatorClick = function ( e
) {
11369 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11370 // Clear the text field
11371 this.setValue( '' );
11378 * Update the 'clear' indicator displayed on type: 'search' text
11379 * fields, hiding it when the field is already empty or when it's not
11382 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11383 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11384 this.setIndicator( null );
11386 this.setIndicator( 'clear' );
11391 * Handle change events.
11395 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11396 this.updateSearchIndicator();
11400 * Handle disable events.
11402 * @param {boolean} disabled Element is disabled
11405 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11406 this.updateSearchIndicator();
11412 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11413 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11414 this.updateSearchIndicator();
11419 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11420 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11421 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11422 * {@link OO.ui.mixin.IndicatorElement indicators}.
11423 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11425 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11428 * // A MultilineTextInputWidget.
11429 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11430 * value: 'Text input on multiple lines'
11432 * $( document.body ).append( multilineTextInput.$element );
11434 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11437 * @extends OO.ui.TextInputWidget
11440 * @param {Object} [config] Configuration options
11441 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11442 * specifies minimum number of rows to display.
11443 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11444 * Use the #maxRows config to specify a maximum number of displayed rows.
11445 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11446 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11448 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11449 config
= $.extend( {
11452 // Parent constructor
11453 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11456 this.autosize
= !!config
.autosize
;
11457 this.styleHeight
= null;
11458 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11459 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11461 // Clone for resizing
11462 if ( this.autosize
) {
11463 this.$clone
= this.$input
11465 .removeAttr( 'id' )
11466 .removeAttr( 'name' )
11467 .insertAfter( this.$input
)
11468 .attr( 'aria-hidden', 'true' )
11469 .addClass( 'oo-ui-element-hidden' );
11473 this.connect( this, {
11478 if ( config
.rows
) {
11479 this.$input
.attr( 'rows', config
.rows
);
11481 if ( this.autosize
) {
11482 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11483 this.isWaitingToBeAttached
= true;
11484 this.installParentChangeDetector();
11490 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11492 /* Static Methods */
11497 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11498 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11499 state
.scrollTop
= config
.$input
.scrollTop();
11508 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11509 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11514 * Handle change events.
11518 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11525 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11526 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11533 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11535 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11537 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11538 // Some platforms emit keycode 10 for Control+Enter keypress in a textarea
11541 this.emit( 'enter', e
);
11546 * Automatically adjust the size of the text input.
11548 * This only affects multiline inputs that are {@link #autosize autosized}.
11551 * @return {OO.ui.Widget} The widget, for chaining
11554 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11555 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11556 idealHeight
, newHeight
, scrollWidth
, property
;
11558 if ( this.$input
.val() !== this.valCache
) {
11559 if ( this.autosize
) {
11561 .val( this.$input
.val() )
11562 .attr( 'rows', this.minRows
)
11563 // Set inline height property to 0 to measure scroll height
11564 .css( 'height', 0 );
11566 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11568 this.valCache
= this.$input
.val();
11570 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11572 // Remove inline height property to measure natural heights
11573 this.$clone
.css( 'height', '' );
11574 innerHeight
= this.$clone
.innerHeight();
11575 outerHeight
= this.$clone
.outerHeight();
11577 // Measure max rows height
11579 .attr( 'rows', this.maxRows
)
11580 .css( 'height', 'auto' )
11582 maxInnerHeight
= this.$clone
.innerHeight();
11584 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11585 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11586 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11587 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11589 this.$clone
.addClass( 'oo-ui-element-hidden' );
11591 // Only apply inline height when expansion beyond natural height is needed
11592 // Use the difference between the inner and outer height as a buffer
11593 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11594 if ( newHeight
!== this.styleHeight
) {
11595 this.$input
.css( 'height', newHeight
);
11596 this.styleHeight
= newHeight
;
11597 this.emit( 'resize' );
11600 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11601 if ( scrollWidth
!== this.scrollWidth
) {
11602 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11604 this.$label
.css( { right
: '', left
: '' } );
11605 this.$indicator
.css( { right
: '', left
: '' } );
11607 if ( scrollWidth
) {
11608 this.$indicator
.css( property
, scrollWidth
);
11609 if ( this.labelPosition
=== 'after' ) {
11610 this.$label
.css( property
, scrollWidth
);
11614 this.scrollWidth
= scrollWidth
;
11615 this.positionLabel();
11625 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11626 return $( '<textarea>' );
11630 * Check if the input automatically adjusts its size.
11632 * @return {boolean}
11634 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11635 return !!this.autosize
;
11641 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11642 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11643 if ( state
.scrollTop
!== undefined ) {
11644 this.$input
.scrollTop( state
.scrollTop
);
11649 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11650 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11651 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11653 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11654 * option, that option will appear to be selected.
11655 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11658 * After the user chooses an option, its `data` will be used as a new value for the widget.
11659 * A `label` also can be specified for each option: if given, it will be shown instead of the
11660 * `data` in the dropdown menu.
11662 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11664 * For more information about menus and options, please see the
11665 * [OOUI documentation on MediaWiki][1].
11668 * // A ComboBoxInputWidget.
11669 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11670 * value: 'Option 1',
11672 * { data: 'Option 1' },
11673 * { data: 'Option 2' },
11674 * { data: 'Option 3' }
11677 * $( document.body ).append( comboBox.$element );
11680 * // Example: A ComboBoxInputWidget with additional option labels.
11681 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11682 * value: 'Option 1',
11685 * data: 'Option 1',
11686 * label: 'Option One'
11689 * data: 'Option 2',
11690 * label: 'Option Two'
11693 * data: 'Option 3',
11694 * label: 'Option Three'
11698 * $( document.body ).append( comboBox.$element );
11700 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11703 * @extends OO.ui.TextInputWidget
11706 * @param {Object} [config] Configuration options
11707 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11708 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu
11710 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
11711 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
11712 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
11713 * uses relative positioning.
11714 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11716 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11717 // Configuration initialization
11718 config
= $.extend( {
11719 autocomplete
: false
11722 // ComboBoxInputWidget shouldn't support `multiline`
11723 config
.multiline
= false;
11725 // See InputWidget#reusePreInfuseDOM about `config.$input`
11726 if ( config
.$input
) {
11727 config
.$input
.removeAttr( 'list' );
11730 // Parent constructor
11731 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11734 this.$overlay
= ( config
.$overlay
=== true ?
11735 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11736 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11737 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11738 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11740 invisibleLabel
: true,
11741 disabled
: this.disabled
11743 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11747 $floatableContainer
: this.$element
,
11748 disabled
: this.isDisabled()
11754 this.connect( this, {
11755 change
: 'onInputChange',
11756 enter
: 'onInputEnter'
11758 this.dropdownButton
.connect( this, {
11759 click
: 'onDropdownButtonClick'
11761 this.menu
.connect( this, {
11762 choose
: 'onMenuChoose',
11763 add
: 'onMenuItemsChange',
11764 remove
: 'onMenuItemsChange',
11765 toggle
: 'onMenuToggle'
11769 this.$input
.attr( {
11771 'aria-owns': this.menu
.getElementId(),
11772 'aria-autocomplete': 'list'
11774 this.dropdownButton
.$button
.attr( {
11775 'aria-controls': this.menu
.getElementId()
11777 // Do not override options set via config.menu.items
11778 if ( config
.options
!== undefined ) {
11779 this.setOptions( config
.options
);
11781 this.$field
= $( '<div>' )
11782 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11783 .append( this.$input
, this.dropdownButton
.$element
);
11785 .addClass( 'oo-ui-comboBoxInputWidget' )
11786 .append( this.$field
);
11787 this.$overlay
.append( this.menu
.$element
);
11788 this.onMenuItemsChange();
11793 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11798 * Get the combobox's menu.
11800 * @return {OO.ui.MenuSelectWidget} Menu widget
11802 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11807 * Get the combobox's text input widget.
11809 * @return {OO.ui.TextInputWidget} Text input widget
11811 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11816 * Handle input change events.
11819 * @param {string} value New value
11821 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11822 var match
= this.menu
.findItemFromData( value
);
11824 this.menu
.selectItem( match
);
11825 if ( this.menu
.findHighlightedItem() ) {
11826 this.menu
.highlightItem( match
);
11829 if ( !this.isDisabled() ) {
11830 this.menu
.toggle( true );
11835 * Handle input enter events.
11839 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11840 if ( !this.isDisabled() ) {
11841 this.menu
.toggle( false );
11846 * Handle button click events.
11850 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11851 this.menu
.toggle();
11856 * Handle menu choose events.
11859 * @param {OO.ui.OptionWidget} item Chosen item
11861 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11862 this.setValue( item
.getData() );
11866 * Handle menu item change events.
11870 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11871 var match
= this.menu
.findItemFromData( this.getValue() );
11872 this.menu
.selectItem( match
);
11873 if ( this.menu
.findHighlightedItem() ) {
11874 this.menu
.highlightItem( match
);
11876 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11880 * Handle menu toggle events.
11883 * @param {boolean} isVisible Open state of the menu
11885 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11886 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11890 * Update the disabled state of the controls
11894 * @return {OO.ui.ComboBoxInputWidget} The widget, for chaining
11896 OO
.ui
.ComboBoxInputWidget
.prototype.updateControlsDisabled = function () {
11897 var disabled
= this.isDisabled() || this.isReadOnly();
11898 if ( this.dropdownButton
) {
11899 this.dropdownButton
.setDisabled( disabled
);
11902 this.menu
.setDisabled( disabled
);
11910 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function () {
11912 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.apply( this, arguments
);
11913 this.updateControlsDisabled();
11920 OO
.ui
.ComboBoxInputWidget
.prototype.setReadOnly = function () {
11922 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
11923 this.updateControlsDisabled();
11928 * Set the options available for this input.
11930 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11932 * @return {OO.ui.Widget} The widget, for chaining
11934 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11937 .addItems( options
.map( function ( opt
) {
11938 return new OO
.ui
.MenuOptionWidget( {
11940 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11948 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11949 * which is a widget that is specified by reference before any optional configuration settings.
11951 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of
11954 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11955 * A left-alignment is used for forms with many fields.
11956 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11957 * A right-alignment is used for long but familiar forms which users tab through,
11958 * verifying the current field with a quick glance at the label.
11959 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11960 * that users fill out from top to bottom.
11961 * - **inline**: The label is placed after the field-widget and aligned to the left.
11962 * An inline-alignment is best used with checkboxes or radio buttons.
11964 * Help text can either be:
11966 * - accessed via a help icon that appears in the upper right corner of the rendered field layout,
11968 * - shown as a subtle explanation below the label.
11970 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`.
11971 * If it is long or not essential, leave `helpInline` to its default, `false`.
11973 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11975 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11978 * @extends OO.ui.Layout
11979 * @mixins OO.ui.mixin.LabelElement
11980 * @mixins OO.ui.mixin.TitledElement
11983 * @param {OO.ui.Widget} fieldWidget Field widget
11984 * @param {Object} [config] Configuration options
11985 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11987 * @cfg {Array} [errors] Error messages about the widget, which will be
11988 * displayed below the widget.
11989 * @cfg {Array} [warnings] Warning messages about the widget, which will be
11990 * displayed below the widget.
11991 * @cfg {Array} [successMessages] Success messages on user interactions with the widget,
11992 * which will be displayed below the widget.
11993 * The array may contain strings or OO.ui.HtmlSnippet instances.
11994 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11995 * below the widget.
11996 * The array may contain strings or OO.ui.HtmlSnippet instances.
11997 * These are more visible than `help` messages when `helpInline` is set, and so
11998 * might be good for transient messages.
11999 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
12000 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
12001 * corner of the rendered field; clicking it will display the text in a popup.
12002 * If `helpInline` is `true`, then a subtle description will be shown after the
12004 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
12005 * or shown when the "help" icon is clicked.
12006 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
12008 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12010 * @throws {Error} An error is thrown if no widget is specified
12012 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
12013 // Allow passing positional parameters inside the config object
12014 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12015 config
= fieldWidget
;
12016 fieldWidget
= config
.fieldWidget
;
12019 // Make sure we have required constructor arguments
12020 if ( fieldWidget
=== undefined ) {
12021 throw new Error( 'Widget not found' );
12024 // Configuration initialization
12025 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
12027 // Parent constructor
12028 OO
.ui
.FieldLayout
.parent
.call( this, config
);
12030 // Mixin constructors
12031 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
12032 $label
: $( '<label>' )
12034 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( { $titled
: this.$label
}, config
) );
12037 this.fieldWidget
= fieldWidget
;
12039 this.warnings
= [];
12040 this.successMessages
= [];
12042 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12043 this.$messages
= $( '<div>' );
12044 this.$header
= $( '<span>' );
12045 this.$body
= $( '<div>' );
12047 this.helpInline
= config
.helpInline
;
12050 this.fieldWidget
.connect( this, {
12051 disable
: 'onFieldDisable'
12055 this.$help
= config
.help
?
12056 this.createHelpElement( config
.help
, config
.$overlay
) :
12058 if ( this.fieldWidget
.getInputId() ) {
12059 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
12060 if ( this.helpInline
) {
12061 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
12064 this.$label
.on( 'click', function () {
12065 this.fieldWidget
.simulateLabelClick();
12067 if ( this.helpInline
) {
12068 this.$help
.on( 'click', function () {
12069 this.fieldWidget
.simulateLabelClick();
12074 .addClass( 'oo-ui-fieldLayout' )
12075 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
12076 .append( this.$body
);
12077 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
12078 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
12079 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
12081 .addClass( 'oo-ui-fieldLayout-field' )
12082 .append( this.fieldWidget
.$element
);
12084 this.setErrors( config
.errors
|| [] );
12085 this.setWarnings( config
.warnings
|| [] );
12086 this.setSuccess( config
.successMessages
|| [] );
12087 this.setNotices( config
.notices
|| [] );
12088 this.setAlignment( config
.align
);
12089 // Call this again to take into account the widget's accessKey
12090 this.updateTitle();
12095 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
12096 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
12097 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
12102 * Handle field disable events.
12105 * @param {boolean} value Field is disabled
12107 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
12108 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
12112 * Get the widget contained by the field.
12114 * @return {OO.ui.Widget} Field widget
12116 OO
.ui
.FieldLayout
.prototype.getField = function () {
12117 return this.fieldWidget
;
12121 * Return `true` if the given field widget can be used with `'inline'` alignment (see
12122 * #setAlignment). Return `false` if it can't or if this can't be determined.
12124 * @return {boolean}
12126 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
12127 // This is very simplistic, but should be good enough.
12128 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
12133 * @param {string} kind 'error' or 'notice'
12134 * @param {string|OO.ui.HtmlSnippet} text
12137 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
12138 return new OO
.ui
.MessageWidget( {
12146 * Set the field alignment mode.
12149 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
12151 * @return {OO.ui.BookletLayout} The layout, for chaining
12153 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
12154 if ( value
!== this.align
) {
12155 // Default to 'left'
12156 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
12160 if ( value
=== 'inline' && !this.isFieldInline() ) {
12163 // Reorder elements
12165 if ( this.helpInline
) {
12166 if ( value
=== 'top' ) {
12167 this.$header
.append( this.$label
);
12168 this.$body
.append( this.$header
, this.$field
, this.$help
);
12169 } else if ( value
=== 'inline' ) {
12170 this.$header
.append( this.$label
, this.$help
);
12171 this.$body
.append( this.$field
, this.$header
);
12173 this.$header
.append( this.$label
, this.$help
);
12174 this.$body
.append( this.$header
, this.$field
);
12177 if ( value
=== 'top' ) {
12178 this.$header
.append( this.$help
, this.$label
);
12179 this.$body
.append( this.$header
, this.$field
);
12180 } else if ( value
=== 'inline' ) {
12181 this.$header
.append( this.$help
, this.$label
);
12182 this.$body
.append( this.$field
, this.$header
);
12184 this.$header
.append( this.$label
);
12185 this.$body
.append( this.$header
, this.$help
, this.$field
);
12188 // Set classes. The following classes can be used here:
12189 // * oo-ui-fieldLayout-align-left
12190 // * oo-ui-fieldLayout-align-right
12191 // * oo-ui-fieldLayout-align-top
12192 // * oo-ui-fieldLayout-align-inline
12193 if ( this.align
) {
12194 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
12196 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
12197 this.align
= value
;
12204 * Set the list of error messages.
12206 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
12207 * The array may contain strings or OO.ui.HtmlSnippet instances.
12209 * @return {OO.ui.BookletLayout} The layout, for chaining
12211 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
12212 this.errors
= errors
.slice();
12213 this.updateMessages();
12218 * Set the list of warning messages.
12220 * @param {Array} warnings Warning messages about the widget, which will be displayed below
12222 * The array may contain strings or OO.ui.HtmlSnippet instances.
12224 * @return {OO.ui.BookletLayout} The layout, for chaining
12226 OO
.ui
.FieldLayout
.prototype.setWarnings = function ( warnings
) {
12227 this.warnings
= warnings
.slice();
12228 this.updateMessages();
12233 * Set the list of success messages.
12235 * @param {Array} successMessages Success messages about the widget, which will be displayed below
12237 * The array may contain strings or OO.ui.HtmlSnippet instances.
12239 * @return {OO.ui.BookletLayout} The layout, for chaining
12241 OO
.ui
.FieldLayout
.prototype.setSuccess = function ( successMessages
) {
12242 this.successMessages
= successMessages
.slice();
12243 this.updateMessages();
12248 * Set the list of notice messages.
12250 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
12251 * The array may contain strings or OO.ui.HtmlSnippet instances.
12253 * @return {OO.ui.BookletLayout} The layout, for chaining
12255 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
12256 this.notices
= notices
.slice();
12257 this.updateMessages();
12262 * Update the rendering of error, warning, success and notice messages.
12266 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
12268 this.$messages
.empty();
12271 this.errors
.length
||
12272 this.warnings
.length
||
12273 this.successMessages
.length
||
12274 this.notices
.length
12276 this.$body
.after( this.$messages
);
12278 this.$messages
.remove();
12282 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
12283 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
12285 for ( i
= 0; i
< this.warnings
.length
; i
++ ) {
12286 this.$messages
.append( this.makeMessage( 'warning', this.warnings
[ i
] ) );
12288 for ( i
= 0; i
< this.successMessages
.length
; i
++ ) {
12289 this.$messages
.append( this.makeMessage( 'success', this.successMessages
[ i
] ) );
12291 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
12292 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
12297 * Include information about the widget's accessKey in our title. TitledElement calls this method.
12298 * (This is a bit of a hack.)
12301 * @param {string} title Tooltip label for 'title' attribute
12304 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
12305 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
12306 return this.fieldWidget
.formatTitleWithAccessKey( title
);
12312 * Creates and returns the help element. Also sets the `aria-describedby`
12313 * attribute on the main element of the `fieldWidget`.
12316 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
12317 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
12318 * @return {jQuery} The element that should become `this.$help`.
12320 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
12321 var helpId
, helpWidget
;
12323 if ( this.helpInline
) {
12324 helpWidget
= new OO
.ui
.LabelWidget( {
12326 classes
: [ 'oo-ui-inline-help' ]
12329 helpId
= helpWidget
.getElementId();
12331 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12332 $overlay
: $overlay
,
12336 classes
: [ 'oo-ui-fieldLayout-help' ],
12339 label
: OO
.ui
.msg( 'ooui-field-help' ),
12340 invisibleLabel
: true
12342 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
12343 helpWidget
.getPopup().$body
.html( help
.toString() );
12345 helpWidget
.getPopup().$body
.text( help
);
12348 helpId
= helpWidget
.getPopup().getBodyId();
12351 // Set the 'aria-describedby' attribute on the fieldWidget
12352 // Preference given to an input or a button
12354 this.fieldWidget
.$input
||
12355 this.fieldWidget
.$button
||
12356 this.fieldWidget
.$element
12357 ).attr( 'aria-describedby', helpId
);
12359 return helpWidget
.$element
;
12363 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget,
12364 * a button, and an optional label and/or help text. The field-widget (e.g., a
12365 * {@link OO.ui.TextInputWidget TextInputWidget}), is required and is specified before any optional
12366 * configuration settings.
12368 * Labels can be aligned in one of four ways:
12370 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12371 * A left-alignment is used for forms with many fields.
12372 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12373 * A right-alignment is used for long but familiar forms which users tab through,
12374 * verifying the current field with a quick glance at the label.
12375 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12376 * that users fill out from top to bottom.
12377 * - **inline**: The label is placed after the field-widget and aligned to the left.
12378 * An inline-alignment is best used with checkboxes or radio buttons.
12380 * Help text is accessed via a help icon that appears in the upper right corner of the rendered
12381 * field layout when help text is specified.
12384 * // Example of an ActionFieldLayout
12385 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12386 * new OO.ui.TextInputWidget( {
12387 * placeholder: 'Field widget'
12389 * new OO.ui.ButtonWidget( {
12393 * label: 'An ActionFieldLayout. This label is aligned top',
12395 * help: 'This is help text'
12399 * $( document.body ).append( actionFieldLayout.$element );
12402 * @extends OO.ui.FieldLayout
12405 * @param {OO.ui.Widget} fieldWidget Field widget
12406 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12407 * @param {Object} config
12409 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12410 // Allow passing positional parameters inside the config object
12411 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12412 config
= fieldWidget
;
12413 fieldWidget
= config
.fieldWidget
;
12414 buttonWidget
= config
.buttonWidget
;
12417 // Parent constructor
12418 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12421 this.buttonWidget
= buttonWidget
;
12422 this.$button
= $( '<span>' );
12423 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12426 this.$element
.addClass( 'oo-ui-actionFieldLayout' );
12428 .addClass( 'oo-ui-actionFieldLayout-button' )
12429 .append( this.buttonWidget
.$element
);
12431 .addClass( 'oo-ui-actionFieldLayout-input' )
12432 .append( this.fieldWidget
.$element
);
12433 this.$field
.append( this.$input
, this.$button
);
12438 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12441 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12442 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12443 * configured with a label as well. For more information and examples,
12444 * please see the [OOUI documentation on MediaWiki][1].
12447 * // Example of a fieldset layout
12448 * var input1 = new OO.ui.TextInputWidget( {
12449 * placeholder: 'A text input field'
12452 * var input2 = new OO.ui.TextInputWidget( {
12453 * placeholder: 'A text input field'
12456 * var fieldset = new OO.ui.FieldsetLayout( {
12457 * label: 'Example of a fieldset layout'
12460 * fieldset.addItems( [
12461 * new OO.ui.FieldLayout( input1, {
12462 * label: 'Field One'
12464 * new OO.ui.FieldLayout( input2, {
12465 * label: 'Field Two'
12468 * $( document.body ).append( fieldset.$element );
12470 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12473 * @extends OO.ui.Layout
12474 * @mixins OO.ui.mixin.IconElement
12475 * @mixins OO.ui.mixin.LabelElement
12476 * @mixins OO.ui.mixin.GroupElement
12479 * @param {Object} [config] Configuration options
12480 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset.
12481 * See OO.ui.FieldLayout for more information about fields.
12482 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
12483 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
12484 * corner of the rendered field; clicking it will display the text in a popup.
12485 * If `helpInline` is `true`, then a subtle description will be shown after the
12487 * For feedback messages, you are advised to use `notices`.
12488 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
12489 * or shown when the "help" icon is clicked.
12490 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12491 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12493 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12496 // Configuration initialization
12497 config
= config
|| {};
12499 // Parent constructor
12500 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12502 // Mixin constructors
12503 OO
.ui
.mixin
.IconElement
.call( this, config
);
12504 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12505 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12508 this.$header
= $( '<legend>' );
12512 .addClass( 'oo-ui-fieldsetLayout-header' )
12513 .append( this.$icon
, this.$label
);
12514 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12516 .addClass( 'oo-ui-fieldsetLayout' )
12517 .prepend( this.$header
, this.$group
);
12520 if ( config
.help
) {
12521 if ( config
.helpInline
) {
12522 helpWidget
= new OO
.ui
.LabelWidget( {
12523 label
: config
.help
,
12524 classes
: [ 'oo-ui-inline-help' ]
12526 this.$element
.prepend( this.$header
, helpWidget
.$element
, this.$group
);
12528 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12529 $overlay
: config
.$overlay
,
12533 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12536 label
: OO
.ui
.msg( 'ooui-field-help' ),
12537 invisibleLabel
: true
12539 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12540 helpWidget
.getPopup().$body
.html( config
.help
.toString() );
12542 helpWidget
.getPopup().$body
.text( config
.help
);
12544 this.$header
.append( helpWidget
.$element
);
12547 if ( Array
.isArray( config
.items
) ) {
12548 this.addItems( config
.items
);
12554 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12555 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12556 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12557 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12559 /* Static Properties */
12565 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12568 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use
12569 * browser-based form submission for the fields instead of handling them in JavaScript. Form layouts
12570 * can be configured with an HTML form action, an encoding type, and a method using the #action,
12571 * #enctype, and #method configs, respectively.
12572 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12574 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12575 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12576 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12577 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12578 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12579 * often have simplified APIs to match the capabilities of HTML forms.
12580 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12582 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12583 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12586 * // Example of a form layout that wraps a fieldset layout.
12587 * var input1 = new OO.ui.TextInputWidget( {
12588 * placeholder: 'Username'
12590 * input2 = new OO.ui.TextInputWidget( {
12591 * placeholder: 'Password',
12594 * submit = new OO.ui.ButtonInputWidget( {
12597 * fieldset = new OO.ui.FieldsetLayout( {
12598 * label: 'A form layout'
12601 * fieldset.addItems( [
12602 * new OO.ui.FieldLayout( input1, {
12603 * label: 'Username',
12606 * new OO.ui.FieldLayout( input2, {
12607 * label: 'Password',
12610 * new OO.ui.FieldLayout( submit )
12612 * var form = new OO.ui.FormLayout( {
12613 * items: [ fieldset ],
12614 * action: '/api/formhandler',
12617 * $( document.body ).append( form.$element );
12620 * @extends OO.ui.Layout
12621 * @mixins OO.ui.mixin.GroupElement
12624 * @param {Object} [config] Configuration options
12625 * @cfg {string} [method] HTML form `method` attribute
12626 * @cfg {string} [action] HTML form `action` attribute
12627 * @cfg {string} [enctype] HTML form `enctype` attribute
12628 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12630 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12633 // Configuration initialization
12634 config
= config
|| {};
12636 // Parent constructor
12637 OO
.ui
.FormLayout
.parent
.call( this, config
);
12639 // Mixin constructors
12640 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12643 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12645 // Make sure the action is safe
12646 action
= config
.action
;
12647 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12648 action
= './' + action
;
12653 .addClass( 'oo-ui-formLayout' )
12655 method
: config
.method
,
12657 enctype
: config
.enctype
12659 if ( Array
.isArray( config
.items
) ) {
12660 this.addItems( config
.items
);
12666 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12667 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12672 * A 'submit' event is emitted when the form is submitted.
12677 /* Static Properties */
12683 OO
.ui
.FormLayout
.static.tagName
= 'form';
12688 * Handle form submit events.
12691 * @param {jQuery.Event} e Submit event
12693 * @return {OO.ui.FormLayout} The layout, for chaining
12695 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12696 if ( this.emit( 'submit' ) ) {
12702 * PanelLayouts expand to cover the entire area of their parent. They can be configured with
12703 * scrolling, padding, and a frame, and are often used together with
12704 * {@link OO.ui.StackLayout StackLayouts}.
12707 * // Example of a panel layout
12708 * var panel = new OO.ui.PanelLayout( {
12712 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12714 * $( document.body ).append( panel.$element );
12717 * @extends OO.ui.Layout
12720 * @param {Object} [config] Configuration options
12721 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12722 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12723 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12724 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside
12727 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12728 // Configuration initialization
12729 config
= $.extend( {
12736 // Parent constructor
12737 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12740 this.$element
.addClass( 'oo-ui-panelLayout' );
12741 if ( config
.scrollable
) {
12742 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12744 if ( config
.padded
) {
12745 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12747 if ( config
.expanded
) {
12748 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12750 if ( config
.framed
) {
12751 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12757 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12759 /* Static Methods */
12764 OO
.ui
.PanelLayout
.static.reusePreInfuseDOM = function ( node
, config
) {
12765 config
= OO
.ui
.PanelLayout
.parent
.static.reusePreInfuseDOM( node
, config
);
12766 if ( config
.preserveContent
!== false ) {
12767 config
.$content
= $( node
).contents();
12775 * Focus the panel layout
12777 * The default implementation just focuses the first focusable element in the panel
12779 OO
.ui
.PanelLayout
.prototype.focus = function () {
12780 OO
.ui
.findFocusable( this.$element
).focus();
12784 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12785 * items), with small margins between them. Convenient when you need to put a number of block-level
12786 * widgets on a single line next to each other.
12788 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12791 * // HorizontalLayout with a text input and a label.
12792 * var layout = new OO.ui.HorizontalLayout( {
12794 * new OO.ui.LabelWidget( { label: 'Label' } ),
12795 * new OO.ui.TextInputWidget( { value: 'Text' } )
12798 * $( document.body ).append( layout.$element );
12801 * @extends OO.ui.Layout
12802 * @mixins OO.ui.mixin.GroupElement
12805 * @param {Object} [config] Configuration options
12806 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12808 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12809 // Configuration initialization
12810 config
= config
|| {};
12812 // Parent constructor
12813 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12815 // Mixin constructors
12816 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12819 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12820 if ( Array
.isArray( config
.items
) ) {
12821 this.addItems( config
.items
);
12827 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12828 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12831 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12832 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12833 * (to adjust the value in increments) to allow the user to enter a number.
12836 * // A NumberInputWidget.
12837 * var numberInput = new OO.ui.NumberInputWidget( {
12838 * label: 'NumberInputWidget',
12839 * input: { value: 5 },
12843 * $( document.body ).append( numberInput.$element );
12846 * @extends OO.ui.TextInputWidget
12849 * @param {Object} [config] Configuration options
12850 * @cfg {Object} [minusButton] Configuration options to pass to the
12851 * {@link OO.ui.ButtonWidget decrementing button widget}.
12852 * @cfg {Object} [plusButton] Configuration options to pass to the
12853 * {@link OO.ui.ButtonWidget incrementing button widget}.
12854 * @cfg {number} [min=-Infinity] Minimum allowed value
12855 * @cfg {number} [max=Infinity] Maximum allowed value
12856 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12857 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or Up/Down arrow keys.
12858 * Defaults to `step` if specified, otherwise `1`.
12859 * @cfg {number} [pageStep=10*buttonStep] Delta when using the Page-up/Page-down keys.
12860 * Defaults to 10 times `buttonStep`.
12861 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12863 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12864 var $field
= $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' );
12866 // Configuration initialization
12867 config
= $.extend( {
12873 // For backward compatibility
12874 $.extend( config
, config
.input
);
12877 // Parent constructor
12878 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12882 if ( config
.showButtons
) {
12883 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12885 disabled
: this.isDisabled(),
12887 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12892 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12893 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12895 disabled
: this.isDisabled(),
12897 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12902 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12907 keydown
: this.onKeyDown
.bind( this ),
12908 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12910 if ( config
.showButtons
) {
12911 this.plusButton
.connect( this, {
12912 click
: [ 'onButtonClick', +1 ]
12914 this.minusButton
.connect( this, {
12915 click
: [ 'onButtonClick', -1 ]
12920 $field
.append( this.$input
);
12921 if ( config
.showButtons
) {
12923 .prepend( this.minusButton
.$element
)
12924 .append( this.plusButton
.$element
);
12928 if ( config
.allowInteger
|| config
.isInteger
) {
12929 // Backward compatibility
12932 this.setRange( config
.min
, config
.max
);
12933 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
12934 // Set the validation method after we set step and range
12935 // so that it doesn't immediately call setValidityFlag
12936 this.setValidation( this.validateNumber
.bind( this ) );
12939 .addClass( 'oo-ui-numberInputWidget' )
12940 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12946 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12950 // Backward compatibility
12951 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12952 this.setStep( flag
? 1 : null );
12954 // Backward compatibility
12955 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12957 // Backward compatibility
12958 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12959 return this.step
=== 1;
12961 // Backward compatibility
12962 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12965 * Set the range of allowed values
12967 * @param {number} min Minimum allowed value
12968 * @param {number} max Maximum allowed value
12970 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12972 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12976 this.$input
.attr( 'min', this.min
);
12977 this.$input
.attr( 'max', this.max
);
12978 this.setValidityFlag();
12982 * Get the current range
12984 * @return {number[]} Minimum and maximum values
12986 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12987 return [ this.min
, this.max
];
12991 * Set the stepping deltas
12993 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12994 * Defaults to `step` if specified, otherwise `1`.
12995 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12996 * Defaults to 10 times `buttonStep`.
12997 * @param {number|null} [step] If specified, the field only accepts values that are multiples
13000 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
13001 if ( buttonStep
=== undefined ) {
13002 buttonStep
= step
|| 1;
13004 if ( pageStep
=== undefined ) {
13005 pageStep
= 10 * buttonStep
;
13007 if ( step
!== null && step
<= 0 ) {
13008 throw new Error( 'Step value, if given, must be positive' );
13010 if ( buttonStep
<= 0 ) {
13011 throw new Error( 'Button step value must be positive' );
13013 if ( pageStep
<= 0 ) {
13014 throw new Error( 'Page step value must be positive' );
13017 this.buttonStep
= buttonStep
;
13018 this.pageStep
= pageStep
;
13019 this.$input
.attr( 'step', this.step
|| 'any' );
13020 this.setValidityFlag();
13026 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
13027 if ( value
=== '' ) {
13028 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
13029 // so here we make sure an 'empty' value is actually displayed as such.
13030 this.$input
.val( '' );
13032 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
13036 * Get the current stepping values
13038 * @return {number[]} Button step, page step, and validity step
13040 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
13041 return [ this.buttonStep
, this.pageStep
, this.step
];
13045 * Get the current value of the widget as a number
13047 * @return {number} May be NaN, or an invalid number
13049 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
13050 return +this.getValue();
13054 * Adjust the value of the widget
13056 * @param {number} delta Adjustment amount
13058 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
13059 var n
, v
= this.getNumericValue();
13062 if ( isNaN( delta
) || !isFinite( delta
) ) {
13063 throw new Error( 'Delta must be a finite number' );
13066 if ( isNaN( v
) ) {
13070 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
13072 n
= Math
.round( n
/ this.step
) * this.step
;
13077 this.setValue( n
);
13084 * @param {string} value Field value
13085 * @return {boolean}
13087 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
13089 if ( value
=== '' ) {
13090 return !this.isRequired();
13093 if ( isNaN( n
) || !isFinite( n
) ) {
13097 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
13101 if ( n
< this.min
|| n
> this.max
) {
13109 * Handle mouse click events.
13112 * @param {number} dir +1 or -1
13114 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
13115 this.adjustValue( dir
* this.buttonStep
);
13119 * Handle mouse wheel events.
13122 * @param {jQuery.Event} event
13123 * @return {undefined|boolean} False to prevent default if event is handled
13125 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
13128 if ( this.isDisabled() || this.isReadOnly() ) {
13132 if ( this.$input
.is( ':focus' ) ) {
13133 // Standard 'wheel' event
13134 if ( event
.originalEvent
.deltaMode
!== undefined ) {
13135 this.sawWheelEvent
= true;
13137 if ( event
.originalEvent
.deltaY
) {
13138 delta
= -event
.originalEvent
.deltaY
;
13139 } else if ( event
.originalEvent
.deltaX
) {
13140 delta
= event
.originalEvent
.deltaX
;
13143 // Non-standard events
13144 if ( !this.sawWheelEvent
) {
13145 if ( event
.originalEvent
.wheelDeltaX
) {
13146 delta
= -event
.originalEvent
.wheelDeltaX
;
13147 } else if ( event
.originalEvent
.wheelDeltaY
) {
13148 delta
= event
.originalEvent
.wheelDeltaY
;
13149 } else if ( event
.originalEvent
.wheelDelta
) {
13150 delta
= event
.originalEvent
.wheelDelta
;
13151 } else if ( event
.originalEvent
.detail
) {
13152 delta
= -event
.originalEvent
.detail
;
13157 delta
= delta
< 0 ? -1 : 1;
13158 this.adjustValue( delta
* this.buttonStep
);
13166 * Handle key down events.
13169 * @param {jQuery.Event} e Key down event
13170 * @return {undefined|boolean} False to prevent default if event is handled
13172 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
13173 if ( this.isDisabled() || this.isReadOnly() ) {
13177 switch ( e
.which
) {
13178 case OO
.ui
.Keys
.UP
:
13179 this.adjustValue( this.buttonStep
);
13181 case OO
.ui
.Keys
.DOWN
:
13182 this.adjustValue( -this.buttonStep
);
13184 case OO
.ui
.Keys
.PAGEUP
:
13185 this.adjustValue( this.pageStep
);
13187 case OO
.ui
.Keys
.PAGEDOWN
:
13188 this.adjustValue( -this.pageStep
);
13194 * Update the disabled state of the controls
13198 * @return {OO.ui.NumberInputWidget} The widget, for chaining
13200 OO
.ui
.NumberInputWidget
.prototype.updateControlsDisabled = function () {
13201 var disabled
= this.isDisabled() || this.isReadOnly();
13202 if ( this.minusButton
) {
13203 this.minusButton
.setDisabled( disabled
);
13205 if ( this.plusButton
) {
13206 this.plusButton
.setDisabled( disabled
);
13214 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
13216 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13217 this.updateControlsDisabled();
13224 OO
.ui
.NumberInputWidget
.prototype.setReadOnly = function () {
13226 OO
.ui
.NumberInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
13227 this.updateControlsDisabled();
13232 * SelectFileInputWidgets allow for selecting files, using <input type="file">. These
13233 * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
13234 * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
13235 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
13237 * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename.
13240 * // A file select input widget.
13241 * var selectFile = new OO.ui.SelectFileInputWidget();
13242 * $( document.body ).append( selectFile.$element );
13244 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
13247 * @extends OO.ui.InputWidget
13250 * @param {Object} [config] Configuration options
13251 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
13252 * @cfg {boolean} [multiple=false] Allow multiple files to be selected.
13253 * @cfg {string} [placeholder] Text to display when no file is selected.
13254 * @cfg {Object} [button] Config to pass to select file button.
13255 * @cfg {string} [icon] Icon to show next to file info
13257 OO
.ui
.SelectFileInputWidget
= function OoUiSelectFileInputWidget( config
) {
13260 config
= config
|| {};
13262 // Construct buttons before parent method is called (calling setDisabled)
13263 this.selectButton
= new OO
.ui
.ButtonWidget( $.extend( {
13264 $element
: $( '<label>' ),
13265 classes
: [ 'oo-ui-selectFileInputWidget-selectButton' ],
13266 label
: OO
.ui
.msg( 'ooui-selectfile-button-select' )
13267 }, config
.button
) );
13269 // Configuration initialization
13270 config
= $.extend( {
13272 placeholder
: OO
.ui
.msg( 'ooui-selectfile-placeholder' ),
13273 $tabIndexed
: this.selectButton
.$tabIndexed
13276 this.info
= new OO
.ui
.SearchInputWidget( {
13277 classes
: [ 'oo-ui-selectFileInputWidget-info' ],
13278 placeholder
: config
.placeholder
,
13279 // Pass an empty collection so that .focus() always does nothing
13280 $tabIndexed
: $( [] )
13281 } ).setIcon( config
.icon
);
13282 // Set tabindex manually on $input as $tabIndexed has been overridden
13283 this.info
.$input
.attr( 'tabindex', -1 );
13285 // Parent constructor
13286 OO
.ui
.SelectFileInputWidget
.parent
.call( this, config
);
13289 this.currentFiles
= this.filterFiles( this.$input
[ 0 ].files
|| [] );
13290 if ( Array
.isArray( config
.accept
) ) {
13291 this.accept
= config
.accept
;
13293 this.accept
= null;
13295 this.multiple
= !!config
.multiple
;
13298 this.info
.connect( this, { change
: 'onInfoChange' } );
13299 this.selectButton
.$button
.on( {
13300 keypress
: this.onKeyPress
.bind( this )
13303 change
: this.onFileSelected
.bind( this ),
13305 // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
13306 // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
13307 // Since this messes with our custom styling (the file input has large dimensions and this
13308 // causes the label to scroll out of view), scroll the button back to top. (T192131)
13309 focus: function () {
13310 widget
.$input
.parent().prop( 'scrollTop', 0 );
13313 this.connect( this, { change
: 'updateUI' } );
13315 this.fieldLayout
= new OO
.ui
.ActionFieldLayout( this.info
, this.selectButton
, { align
: 'top' } );
13320 // this.selectButton is tabindexed
13322 // Infused input may have previously by
13323 // TabIndexed, so remove aria-disabled attr.
13324 'aria-disabled': null
13327 if ( this.accept
) {
13328 this.$input
.attr( 'accept', this.accept
.join( ', ' ) );
13330 if ( this.multiple
) {
13331 this.$input
.attr( 'multiple', '' );
13333 this.selectButton
.$button
.append( this.$input
);
13336 .addClass( 'oo-ui-selectFileInputWidget' )
13337 .append( this.fieldLayout
.$element
);
13344 OO
.inheritClass( OO
.ui
.SelectFileInputWidget
, OO
.ui
.InputWidget
);
13346 /* Static properties */
13348 // Set empty title so that browser default tooltips like "No file chosen" don't appear.
13349 // On SelectFileWidget this tooltip will often be incorrect, so create a consistent
13350 // experience on SelectFileInputWidget.
13351 OO
.ui
.SelectFileInputWidget
.static.title
= '';
13356 * Get the filename of the currently selected file.
13358 * @return {string} Filename
13360 OO
.ui
.SelectFileInputWidget
.prototype.getFilename = function () {
13361 if ( this.currentFiles
.length
) {
13362 return this.currentFiles
.map( function ( file
) {
13366 // Try to strip leading fakepath.
13367 return this.getValue().split( '\\' ).pop();
13374 OO
.ui
.SelectFileInputWidget
.prototype.setValue = function ( value
) {
13375 if ( value
=== undefined ) {
13376 // Called during init, don't replace value if just infusing.
13380 // We need to update this.value, but without trying to modify
13381 // the DOM value, which would throw an exception.
13382 if ( this.value
!== value
) {
13383 this.value
= value
;
13384 this.emit( 'change', this.value
);
13387 this.currentFiles
= [];
13389 OO
.ui
.SelectFileInputWidget
.super.prototype.setValue
.call( this, '' );
13394 * Handle file selection from the input.
13397 * @param {jQuery.Event} e
13399 OO
.ui
.SelectFileInputWidget
.prototype.onFileSelected = function ( e
) {
13400 this.currentFiles
= this.filterFiles( e
.target
.files
|| [] );
13404 * Update the user interface when a file is selected or unselected.
13408 OO
.ui
.SelectFileInputWidget
.prototype.updateUI = function () {
13409 this.info
.setValue( this.getFilename() );
13413 * Determine if we should accept this file.
13416 * @param {FileList|File[]} files Files to filter
13417 * @return {File[]} Filter files
13419 OO
.ui
.SelectFileInputWidget
.prototype.filterFiles = function ( files
) {
13420 var accept
= this.accept
;
13422 function mimeAllowed( file
) {
13424 mimeType
= file
.type
;
13426 if ( !accept
|| !mimeType
) {
13430 for ( i
= 0; i
< accept
.length
; i
++ ) {
13431 mimeTest
= accept
[ i
];
13432 if ( mimeTest
=== mimeType
) {
13434 } else if ( mimeTest
.substr( -2 ) === '/*' ) {
13435 mimeTest
= mimeTest
.substr( 0, mimeTest
.length
- 1 );
13436 if ( mimeType
.substr( 0, mimeTest
.length
) === mimeTest
) {
13444 return Array
.prototype.filter
.call( files
, mimeAllowed
);
13448 * Handle info input change events
13450 * The info widget can only be changed by the user
13451 * with the clear button.
13454 * @param {string} value
13456 OO
.ui
.SelectFileInputWidget
.prototype.onInfoChange = function ( value
) {
13457 if ( value
=== '' ) {
13458 this.setValue( null );
13463 * Handle key press events.
13466 * @param {jQuery.Event} e Key press event
13467 * @return {undefined|boolean} False to prevent default if event is handled
13469 OO
.ui
.SelectFileInputWidget
.prototype.onKeyPress = function ( e
) {
13470 if ( !this.isDisabled() && this.$input
&&
13471 ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
13473 // Emit a click to open the file selector.
13474 this.$input
.trigger( 'click' );
13475 // Taking focus from the selectButton means keyUp isn't fired, so fire it manually.
13476 this.selectButton
.onDocumentKeyUp( e
);
13484 OO
.ui
.SelectFileInputWidget
.prototype.setDisabled = function ( disabled
) {
13486 OO
.ui
.SelectFileInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13488 this.selectButton
.setDisabled( disabled
);
13489 this.info
.setDisabled( disabled
);
13496 //# sourceMappingURL=oojs-ui-core.js.map.json