2 * OOUI v0.34.0-pre (d5e74518ab)
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-09-04T18:28:52Z
16 * Namespace for all classes, static methods and static properties.
48 * Constants for MouseEvent.which
52 OO
.ui
.MouseButtons
= {
65 * Generate a unique ID for element
69 OO
.ui
.generateElementId = function () {
71 return 'ooui-' + OO
.ui
.elementId
;
75 * Check if an element is focusable.
76 * Inspired by :focusable in jQueryUI v1.11.4 - 2015-04-14
78 * @param {jQuery} $element Element to test
79 * @return {boolean} Element is focusable
81 OO
.ui
.isFocusableElement = function ( $element
) {
83 element
= $element
[ 0 ];
85 // Anything disabled is not focusable
86 if ( element
.disabled
) {
90 // Check if the element is visible
92 // This is quicker than calling $element.is( ':visible' )
93 $.expr
.pseudos
.visible( element
) &&
94 // Check that all parents are visible
95 !$element
.parents().addBack().filter( function () {
96 return $.css( this, 'visibility' ) === 'hidden';
102 // Check if the element is ContentEditable, which is the string 'true'
103 if ( element
.contentEditable
=== 'true' ) {
107 // Anything with a non-negative numeric tabIndex is focusable.
108 // Use .prop to avoid browser bugs
109 if ( $element
.prop( 'tabIndex' ) >= 0 ) {
113 // Some element types are naturally focusable
114 // (indexOf is much faster than regex in Chrome and about the
115 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
116 nodeName
= element
.nodeName
.toLowerCase();
117 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName
) !== -1 ) {
121 // Links and areas are focusable if they have an href
122 if ( ( nodeName
=== 'a' || nodeName
=== 'area' ) && $element
.attr( 'href' ) !== undefined ) {
130 * Find a focusable child.
132 * @param {jQuery} $container Container to search in
133 * @param {boolean} [backwards] Search backwards
134 * @return {jQuery} Focusable child, or an empty jQuery object if none found
136 OO
.ui
.findFocusable = function ( $container
, backwards
) {
137 var $focusable
= $( [] ),
138 // $focusableCandidates is a superset of things that
139 // could get matched by isFocusableElement
140 $focusableCandidates
= $container
141 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
144 $focusableCandidates
= Array
.prototype.reverse
.call( $focusableCandidates
);
147 $focusableCandidates
.each( function () {
148 var $this = $( this );
149 if ( OO
.ui
.isFocusableElement( $this ) ) {
158 * Get the user's language and any fallback languages.
160 * These language codes are used to localize user interface elements in the user's language.
162 * In environments that provide a localization system, this function should be overridden to
163 * return the user's language(s). The default implementation returns English (en) only.
165 * @return {string[]} Language codes, in descending order of priority
167 OO
.ui
.getUserLanguages = function () {
172 * Get a value in an object keyed by language code.
174 * @param {Object.<string,Mixed>} obj Object keyed by language code
175 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
176 * @param {string} [fallback] Fallback code, used if no matching language can be found
177 * @return {Mixed} Local value
179 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
182 // Requested language
186 // Known user language
187 langs
= OO
.ui
.getUserLanguages();
188 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
195 if ( obj
[ fallback
] ) {
196 return obj
[ fallback
];
198 // First existing language
199 for ( lang
in obj
) {
207 * Check if a node is contained within another node.
209 * Similar to jQuery#contains except a list of containers can be supplied
210 * and a boolean argument allows you to include the container in the match list
212 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
213 * @param {HTMLElement} contained Node to find
214 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match,
215 * otherwise only match descendants
216 * @return {boolean} The node is in the list of target nodes
218 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
220 if ( !Array
.isArray( containers
) ) {
221 containers
= [ containers
];
223 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
225 ( matchContainers
&& contained
=== containers
[ i
] ) ||
226 $.contains( containers
[ i
], contained
)
235 * Return a function, that, as long as it continues to be invoked, will not
236 * be triggered. The function will be called after it stops being called for
237 * N milliseconds. If `immediate` is passed, trigger the function on the
238 * leading edge, instead of the trailing.
240 * Ported from: http://underscorejs.org/underscore.js
242 * @param {Function} func Function to debounce
243 * @param {number} [wait=0] Wait period in milliseconds
244 * @param {boolean} [immediate] Trigger on leading edge
245 * @return {Function} Debounced function
247 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
252 later = function () {
255 func
.apply( context
, args
);
258 if ( immediate
&& !timeout
) {
259 func
.apply( context
, args
);
261 if ( !timeout
|| wait
) {
262 clearTimeout( timeout
);
263 timeout
= setTimeout( later
, wait
);
269 * Puts a console warning with provided message.
271 * @param {string} message Message
273 OO
.ui
.warnDeprecation = function ( message
) {
274 if ( OO
.getProp( window
, 'console', 'warn' ) !== undefined ) {
275 // eslint-disable-next-line no-console
276 console
.warn( message
);
281 * Returns a function, that, when invoked, will only be triggered at most once
282 * during a given window of time. If called again during that window, it will
283 * wait until the window ends and then trigger itself again.
285 * As it's not knowable to the caller whether the function will actually run
286 * when the wrapper is called, return values from the function are entirely
289 * @param {Function} func Function to throttle
290 * @param {number} wait Throttle window length, in milliseconds
291 * @return {Function} Throttled function
293 OO
.ui
.throttle = function ( func
, wait
) {
294 var context
, args
, timeout
,
295 previous
= Date
.now() - wait
,
298 previous
= Date
.now();
299 func
.apply( context
, args
);
302 // Check how long it's been since the last time the function was
303 // called, and whether it's more or less than the requested throttle
304 // period. If it's less, run the function immediately. If it's more,
305 // set a timeout for the remaining time -- but don't replace an
306 // existing timeout, since that'd indefinitely prolong the wait.
307 var remaining
= Math
.max( wait
- ( Date
.now() - previous
), 0 );
311 // If time is up, do setTimeout( run, 0 ) so the function
312 // always runs asynchronously, just like Promise#then .
313 timeout
= setTimeout( run
, remaining
);
319 * Reconstitute a JavaScript object corresponding to a widget created by
320 * the PHP implementation.
322 * This is an alias for `OO.ui.Element.static.infuse()`.
324 * @param {string|HTMLElement|jQuery} idOrNode
325 * A DOM id (if a string) or node for the widget to infuse.
326 * @param {Object} [config] Configuration options
327 * @return {OO.ui.Element}
328 * The `OO.ui.Element` corresponding to this (infusable) document node.
330 OO
.ui
.infuse = function ( idOrNode
, config
) {
331 return OO
.ui
.Element
.static.infuse( idOrNode
, config
);
335 * Get a localized message.
337 * After the message key, message parameters may optionally be passed. In the default
338 * implementation, any occurrences of $1 are replaced with the first parameter, $2 with the
339 * second parameter, etc.
340 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long
341 * as they support unnamed, ordered message parameters.
343 * In environments that provide a localization system, this function should be overridden to
344 * return the message translated in the user's language. The default implementation always
345 * returns English messages. An example of doing this with
346 * [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) follows.
349 * var i, iLen, button,
350 * messagePath = 'oojs-ui/dist/i18n/',
351 * languages = [ $.i18n().locale, 'ur', 'en' ],
354 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
355 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
358 * $.i18n().load( languageMap ).done( function() {
359 * // Replace the built-in `msg` only once we've loaded the internationalization.
360 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
361 * // you put off creating any widgets until this promise is complete, no English
362 * // will be displayed.
363 * OO.ui.msg = $.i18n;
365 * // A button displaying "OK" in the default locale
366 * button = new OO.ui.ButtonWidget( {
367 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
370 * $( document.body ).append( button.$element );
372 * // A button displaying "OK" in Urdu
373 * $.i18n().locale = 'ur';
374 * button = new OO.ui.ButtonWidget( {
375 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
378 * $( document.body ).append( button.$element );
381 * @param {string} key Message key
382 * @param {...Mixed} [params] Message parameters
383 * @return {string} Translated message with parameters substituted
385 OO
.ui
.msg = function ( key
) {
386 // `OO.ui.msg.messages` is defined in code generated during the build process
387 var messages
= OO
.ui
.msg
.messages
,
388 message
= messages
[ key
],
389 params
= Array
.prototype.slice
.call( arguments
, 1 );
390 if ( typeof message
=== 'string' ) {
391 // Perform $1 substitution
392 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
393 var i
= parseInt( n
, 10 );
394 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
397 // Return placeholder if message not found
398 message
= '[' + key
+ ']';
404 * Package a message and arguments for deferred resolution.
406 * Use this when you are statically specifying a message and the message may not yet be present.
408 * @param {string} key Message key
409 * @param {...Mixed} [params] Message parameters
410 * @return {Function} Function that returns the resolved message when executed
412 OO
.ui
.deferMsg = function () {
413 var args
= arguments
;
415 return OO
.ui
.msg
.apply( OO
.ui
, args
);
422 * If the message is a function it will be executed, otherwise it will pass through directly.
424 * @param {Function|string} msg Deferred message, or message text
425 * @return {string} Resolved message
427 OO
.ui
.resolveMsg = function ( msg
) {
428 if ( typeof msg
=== 'function' ) {
435 * @param {string} url
438 OO
.ui
.isSafeUrl = function ( url
) {
439 // Keep this function in sync with php/Tag.php
440 var i
, protocolWhitelist
;
442 function stringStartsWith( haystack
, needle
) {
443 return haystack
.substr( 0, needle
.length
) === needle
;
446 protocolWhitelist
= [
447 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
448 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
449 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
456 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
457 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
462 // This matches '//' too
463 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
466 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
474 * Check if the user has a 'mobile' device.
476 * For our purposes this means the user is primarily using an
477 * on-screen keyboard, touch input instead of a mouse and may
478 * have a physically small display.
480 * It is left up to implementors to decide how to compute this
481 * so the default implementation always returns false.
483 * @return {boolean} User is on a mobile device
485 OO
.ui
.isMobile = function () {
490 * Get the additional spacing that should be taken into account when displaying elements that are
491 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
492 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
494 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
495 * the extra spacing from that edge of viewport (in pixels)
497 OO
.ui
.getViewportSpacing = function () {
507 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
508 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
510 * @return {jQuery} Default overlay node
512 OO
.ui
.getDefaultOverlay = function () {
513 if ( !OO
.ui
.$defaultOverlay
) {
514 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
515 $( document
.body
).append( OO
.ui
.$defaultOverlay
);
517 return OO
.ui
.$defaultOverlay
;
521 * Message store for the default implementation of OO.ui.msg.
523 * Environments that provide a localization system should not use this, but should override
524 * OO.ui.msg altogether.
528 OO
.ui
.msg
.messages
= {
529 "ooui-outline-control-move-down": "Move item down",
530 "ooui-outline-control-move-up": "Move item up",
531 "ooui-outline-control-remove": "Remove item",
532 "ooui-toolbar-more": "More",
533 "ooui-toolgroup-expand": "More",
534 "ooui-toolgroup-collapse": "Fewer",
535 "ooui-item-remove": "Remove",
536 "ooui-dialog-message-accept": "OK",
537 "ooui-dialog-message-reject": "Cancel",
538 "ooui-dialog-process-error": "Something went wrong",
539 "ooui-dialog-process-dismiss": "Dismiss",
540 "ooui-dialog-process-retry": "Try again",
541 "ooui-dialog-process-continue": "Continue",
542 "ooui-combobox-button-label": "Dropdown for combobox",
543 "ooui-selectfile-button-select": "Select a file",
544 "ooui-selectfile-not-supported": "File selection is not supported",
545 "ooui-selectfile-placeholder": "No file is selected",
546 "ooui-selectfile-dragdrop-placeholder": "Drop file here",
547 "ooui-field-help": "Help"
555 * Namespace for OOUI mixins.
557 * Mixins are named according to the type of object they are intended to
558 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
559 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
560 * is intended to be mixed in to an instance of OO.ui.Widget.
568 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
569 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not
570 * have events connected to them and can't be interacted with.
576 * @param {Object} [config] Configuration options
577 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are
578 * added to the top level (e.g., the outermost div) of the element. See the
579 * [OOUI documentation on MediaWiki][2] for an example.
580 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
581 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
582 * @cfg {string} [text] Text to insert
583 * @cfg {Array} [content] An array of content elements to append (after #text).
584 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
585 * Instances of OO.ui.Element will have their $element appended.
586 * @cfg {jQuery} [$content] Content elements to append (after #text).
587 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
588 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number,
590 * Data can also be specified with the #setData method.
592 OO
.ui
.Element
= function OoUiElement( config
) {
593 if ( OO
.ui
.isDemo
) {
594 this.initialConfig
= config
;
596 // Configuration initialization
597 config
= config
|| {};
600 this.elementId
= null;
602 this.data
= config
.data
;
603 this.$element
= config
.$element
||
604 $( document
.createElement( this.getTagName() ) );
605 this.elementGroup
= null;
608 if ( Array
.isArray( config
.classes
) ) {
609 this.$element
.addClass( config
.classes
);
612 this.setElementId( config
.id
);
615 this.$element
.text( config
.text
);
617 if ( config
.content
) {
618 // The `content` property treats plain strings as text; use an
619 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
620 // appropriate $element appended.
621 this.$element
.append( config
.content
.map( function ( v
) {
622 if ( typeof v
=== 'string' ) {
623 // Escape string so it is properly represented in HTML.
624 // Don't create empty text nodes for empty strings.
625 return v
? document
.createTextNode( v
) : undefined;
626 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
629 } else if ( v
instanceof OO
.ui
.Element
) {
635 if ( config
.$content
) {
636 // The `$content` property treats plain strings as HTML.
637 this.$element
.append( config
.$content
);
643 OO
.initClass( OO
.ui
.Element
);
645 /* Static Properties */
648 * The name of the HTML tag used by the element.
650 * The static value may be ignored if the #getTagName method is overridden.
656 OO
.ui
.Element
.static.tagName
= 'div';
661 * Reconstitute a JavaScript object corresponding to a widget created
662 * by the PHP implementation.
664 * @param {string|HTMLElement|jQuery} idOrNode
665 * A DOM id (if a string) or node for the widget to infuse.
666 * @param {Object} [config] Configuration options
667 * @return {OO.ui.Element}
668 * The `OO.ui.Element` corresponding to this (infusable) document node.
669 * For `Tag` objects emitted on the HTML side (used occasionally for content)
670 * the value returned is a newly-created Element wrapping around the existing
673 OO
.ui
.Element
.static.infuse = function ( idOrNode
, config
) {
674 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, config
, false );
676 if ( typeof idOrNode
=== 'string' ) {
677 // IDs deprecated since 0.29.7
678 OO
.ui
.warnDeprecation(
679 'Passing a string ID to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
682 // Verify that the type matches up.
683 // FIXME: uncomment after T89721 is fixed, see T90929.
685 if ( !( obj instanceof this['class'] ) ) {
686 throw new Error( 'Infusion type mismatch!' );
693 * Implementation helper for `infuse`; skips the type check and has an
694 * extra property so that only the top-level invocation touches the DOM.
697 * @param {string|HTMLElement|jQuery} idOrNode
698 * @param {Object} [config] Configuration options
699 * @param {jQuery.Promise} [domPromise] A promise that will be resolved
700 * when the top-level widget of this infusion is inserted into DOM,
701 * replacing the original node; only used internally.
702 * @return {OO.ui.Element}
704 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, config
, domPromise
) {
705 // look for a cached result of a previous infusion.
706 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
707 if ( typeof idOrNode
=== 'string' ) {
709 $elem
= $( document
.getElementById( id
) );
711 $elem
= $( idOrNode
);
712 id
= $elem
.attr( 'id' );
714 if ( !$elem
.length
) {
715 if ( typeof idOrNode
=== 'string' ) {
716 error
= 'Widget not found: ' + idOrNode
;
717 } else if ( idOrNode
&& idOrNode
.selector
) {
718 error
= 'Widget not found: ' + idOrNode
.selector
;
720 error
= 'Widget not found';
722 throw new Error( error
);
724 if ( $elem
[ 0 ].oouiInfused
) {
725 $elem
= $elem
[ 0 ].oouiInfused
;
727 data
= $elem
.data( 'ooui-infused' );
730 if ( data
=== true ) {
731 throw new Error( 'Circular dependency! ' + id
);
734 // Pick up dynamic state, like focus, value of form inputs, scroll position, etc.
735 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
736 // Restore dynamic state after the new element is re-inserted into DOM under
738 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
739 infusedChildren
= $elem
.data( 'ooui-infused-children' );
740 if ( infusedChildren
&& infusedChildren
.length
) {
741 infusedChildren
.forEach( function ( data
) {
742 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
743 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
749 data
= $elem
.attr( 'data-ooui' );
751 throw new Error( 'No infusion data found: ' + id
);
754 data
= JSON
.parse( data
);
758 if ( !( data
&& data
._
) ) {
759 throw new Error( 'No valid infusion data found: ' + id
);
761 if ( data
._
=== 'Tag' ) {
762 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
763 return new OO
.ui
.Element( $.extend( {}, config
, { $element
: $elem
} ) );
765 parts
= data
._
.split( '.' );
766 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
767 if ( cls
=== undefined ) {
768 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
771 // Verify that we're creating an OO.ui.Element instance
774 while ( parent
!== undefined ) {
775 if ( parent
=== OO
.ui
.Element
) {
780 parent
= parent
.parent
;
783 if ( parent
!== OO
.ui
.Element
) {
784 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
789 domPromise
= top
.promise();
791 $elem
.data( 'ooui-infused', true ); // prevent loops
792 data
.id
= id
; // implicit
793 infusedChildren
= [];
794 data
= OO
.copy( data
, null, function deserialize( value
) {
796 if ( OO
.isPlainObject( value
) ) {
798 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, config
, domPromise
);
799 infusedChildren
.push( infused
);
800 // Flatten the structure
801 infusedChildren
.push
.apply(
803 infused
.$element
.data( 'ooui-infused-children' ) || []
805 infused
.$element
.removeData( 'ooui-infused-children' );
808 if ( value
.html
!== undefined ) {
809 return new OO
.ui
.HtmlSnippet( value
.html
);
813 // allow widgets to reuse parts of the DOM
814 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
815 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
816 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
818 // eslint-disable-next-line new-cap
819 obj
= new cls( $.extend( {}, config
, data
) );
820 // If anyone is holding a reference to the old DOM element,
821 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
822 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
823 $elem
[ 0 ].oouiInfused
= obj
.$element
;
824 // now replace old DOM with this new DOM.
826 // An efficient constructor might be able to reuse the entire DOM tree of the original
827 // element, so only mutate the DOM if we need to.
828 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
829 $elem
.replaceWith( obj
.$element
);
833 obj
.$element
.data( 'ooui-infused', obj
);
834 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
835 // set the 'data-ooui' attribute so we can identify infused widgets
836 obj
.$element
.attr( 'data-ooui', '' );
837 // restore dynamic state after the new element is inserted into DOM
838 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
843 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
845 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
846 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
847 * constructor, which will be given the enhanced config.
850 * @param {HTMLElement} node
851 * @param {Object} config
854 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
859 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM
860 * node (and its children) that represent an Element of the same class and the given configuration,
861 * generated by the PHP implementation.
863 * This method is called just before `node` is detached from the DOM. The return value of this
864 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
865 * is inserted into DOM to replace `node`.
868 * @param {HTMLElement} node
869 * @param {Object} config
872 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
877 * Get the document of an element.
880 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
881 * @return {HTMLDocument|null} Document object
883 OO
.ui
.Element
.static.getDocument = function ( obj
) {
884 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
885 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
886 // Empty jQuery selections might have a context
893 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
898 * Get the window of an element or document.
901 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
902 * @return {Window} Window object
904 OO
.ui
.Element
.static.getWindow = function ( obj
) {
905 var doc
= this.getDocument( obj
);
906 return doc
.defaultView
;
910 * Get the direction of an element or document.
913 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
914 * @return {string} Text direction, either 'ltr' or 'rtl'
916 OO
.ui
.Element
.static.getDir = function ( obj
) {
919 if ( obj
instanceof $ ) {
922 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
923 isWin
= obj
.document
!== undefined;
924 if ( isDoc
|| isWin
) {
930 return $( obj
).css( 'direction' );
934 * Get the offset between two frames.
936 * TODO: Make this function not use recursion.
939 * @param {Window} from Window of the child frame
940 * @param {Window} [to=window] Window of the parent frame
941 * @param {Object} [offset] Offset to start with, used internally
942 * @return {Object} Offset object, containing left and top properties
944 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
945 var i
, len
, frames
, frame
, rect
;
951 offset
= { top
: 0, left
: 0 };
953 if ( from.parent
=== from ) {
957 // Get iframe element
958 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
959 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
960 if ( frames
[ i
].contentWindow
=== from ) {
966 // Recursively accumulate offset values
968 rect
= frame
.getBoundingClientRect();
969 offset
.left
+= rect
.left
;
970 offset
.top
+= rect
.top
;
972 this.getFrameOffset( from.parent
, offset
);
979 * Get the offset between two elements.
981 * The two elements may be in a different frame, but in that case the frame $element is in must
982 * be contained in the frame $anchor is in.
985 * @param {jQuery} $element Element whose position to get
986 * @param {jQuery} $anchor Element to get $element's position relative to
987 * @return {Object} Translated position coordinates, containing top and left properties
989 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
990 var iframe
, iframePos
,
991 pos
= $element
.offset(),
992 anchorPos
= $anchor
.offset(),
993 elementDocument
= this.getDocument( $element
),
994 anchorDocument
= this.getDocument( $anchor
);
996 // If $element isn't in the same document as $anchor, traverse up
997 while ( elementDocument
!== anchorDocument
) {
998 iframe
= elementDocument
.defaultView
.frameElement
;
1000 throw new Error( '$element frame is not contained in $anchor frame' );
1002 iframePos
= $( iframe
).offset();
1003 pos
.left
+= iframePos
.left
;
1004 pos
.top
+= iframePos
.top
;
1005 elementDocument
= iframe
.ownerDocument
;
1007 pos
.left
-= anchorPos
.left
;
1008 pos
.top
-= anchorPos
.top
;
1013 * Get element border sizes.
1016 * @param {HTMLElement} el Element to measure
1017 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1019 OO
.ui
.Element
.static.getBorders = function ( el
) {
1020 var doc
= el
.ownerDocument
,
1021 win
= doc
.defaultView
,
1022 style
= win
.getComputedStyle( el
, null ),
1024 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1025 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1026 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1027 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1038 * Get dimensions of an element or window.
1041 * @param {HTMLElement|Window} el Element to measure
1042 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1044 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1046 doc
= el
.ownerDocument
|| el
.document
,
1047 win
= doc
.defaultView
;
1049 if ( win
=== el
|| el
=== doc
.documentElement
) {
1052 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1054 top
: $win
.scrollTop(),
1055 left
: OO
.ui
.Element
.static.getScrollLeft( win
)
1057 scrollbar
: { right
: 0, bottom
: 0 },
1061 bottom
: $win
.innerHeight(),
1062 right
: $win
.innerWidth()
1068 borders
: this.getBorders( el
),
1070 top
: $el
.scrollTop(),
1071 left
: OO
.ui
.Element
.static.getScrollLeft( el
)
1074 right
: $el
.innerWidth() - el
.clientWidth
,
1075 bottom
: $el
.innerHeight() - el
.clientHeight
1077 rect
: el
.getBoundingClientRect()
1083 var rtlScrollType
= null;
1085 // Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1086 // Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1087 function rtlScrollTypeTest() {
1088 var $definer
= $( '<div>' ).attr( {
1090 style
: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
1092 definer
= $definer
[ 0 ];
1094 $definer
.appendTo( 'body' );
1095 if ( definer
.scrollLeft
> 0 ) {
1097 rtlScrollType
= 'default';
1099 definer
.scrollLeft
= 1;
1100 if ( definer
.scrollLeft
=== 0 ) {
1101 // Firefox, old Opera
1102 rtlScrollType
= 'negative';
1104 // Internet Explorer, Edge
1105 rtlScrollType
= 'reverse';
1111 function isRoot( el
) {
1112 return el
.window
=== el
||
1113 el
=== el
.ownerDocument
.body
||
1114 el
=== el
.ownerDocument
.documentElement
;
1118 * Convert native `scrollLeft` value to a value consistent between browsers. See #getScrollLeft.
1119 * @param {number} nativeOffset Native `scrollLeft` value
1120 * @param {HTMLElement|Window} el Element from which the value was obtained
1123 OO
.ui
.Element
.static.computeNormalizedScrollLeft = function ( nativeOffset
, el
) {
1124 // All browsers use the correct scroll type ('negative') on the root, so don't
1125 // do any fixups when looking at the root element
1126 var direction
= isRoot( el
) ? 'ltr' : $( el
).css( 'direction' );
1128 if ( direction
=== 'rtl' ) {
1129 if ( rtlScrollType
=== null ) {
1130 rtlScrollTypeTest();
1132 if ( rtlScrollType
=== 'reverse' ) {
1133 return -nativeOffset
;
1134 } else if ( rtlScrollType
=== 'default' ) {
1135 return nativeOffset
- el
.scrollWidth
+ el
.clientWidth
;
1139 return nativeOffset
;
1143 * Convert our normalized `scrollLeft` value to a value for current browser. See #getScrollLeft.
1144 * @param {number} normalizedOffset Normalized `scrollLeft` value
1145 * @param {HTMLElement|Window} el Element on which the value will be set
1148 OO
.ui
.Element
.static.computeNativeScrollLeft = function ( normalizedOffset
, el
) {
1149 // All browsers use the correct scroll type ('negative') on the root, so don't
1150 // do any fixups when looking at the root element
1151 var direction
= isRoot( el
) ? 'ltr' : $( el
).css( 'direction' );
1153 if ( direction
=== 'rtl' ) {
1154 if ( rtlScrollType
=== null ) {
1155 rtlScrollTypeTest();
1157 if ( rtlScrollType
=== 'reverse' ) {
1158 return -normalizedOffset
;
1159 } else if ( rtlScrollType
=== 'default' ) {
1160 return normalizedOffset
+ el
.scrollWidth
- el
.clientWidth
;
1164 return normalizedOffset
;
1168 * Get the number of pixels that an element's content is scrolled to the left.
1170 * This function smooths out browser inconsistencies (nicely described in the README at
1171 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1172 * with Firefox's 'scrollLeft', which seems the sanest.
1174 * (Firefox's scrollLeft handling is nice because it increases from left to right, consistently
1175 * with `getBoundingClientRect().left` and related APIs; because initial value is zero, so
1176 * resetting it is easy; because adapting a hardcoded scroll position to a symmetrical RTL
1177 * interface requires just negating it, rather than involving `clientWidth` and `scrollWidth`;
1178 * and because if you mess up and don't adapt your code to RTL, it will scroll to the beginning
1179 * rather than somewhere randomly in the middle but not where you wanted.)
1183 * @param {HTMLElement|Window} el Element to measure
1184 * @return {number} Scroll position from the left.
1185 * If the element's direction is LTR, this is a positive number between `0` (initial scroll
1186 * position) and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1187 * If the element's direction is RTL, this is a negative number between `0` (initial scroll
1188 * position) and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1190 OO
.ui
.Element
.static.getScrollLeft = function ( el
) {
1191 var scrollLeft
= isRoot( el
) ? $( window
).scrollLeft() : el
.scrollLeft
;
1192 scrollLeft
= OO
.ui
.Element
.static.computeNormalizedScrollLeft( scrollLeft
, el
);
1197 * Set the number of pixels that an element's content is scrolled to the left.
1199 * See #getScrollLeft.
1203 * @param {HTMLElement|Window} el Element to scroll (and to use in calculations)
1204 * @param {number} scrollLeft Scroll position from the left.
1205 * If the element's direction is LTR, this must be a positive number between `0` (initial scroll
1206 * position) and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1207 * If the element's direction is RTL, this must be a negative number between `0` (initial scroll
1208 * position) and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1210 OO
.ui
.Element
.static.setScrollLeft = function ( el
, scrollLeft
) {
1211 scrollLeft
= OO
.ui
.Element
.static.computeNativeScrollLeft( scrollLeft
, el
);
1212 if ( isRoot( el
) ) {
1213 $( window
).scrollLeft( scrollLeft
);
1215 el
.scrollLeft
= scrollLeft
;
1221 * Get the root scrollable element of given element's document.
1223 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1224 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1225 * lets us use 'body' or 'documentElement' based on what is working.
1227 * https://code.google.com/p/chromium/issues/detail?id=303131
1230 * @param {HTMLElement} el Element to find root scrollable parent for
1231 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1232 * depending on browser
1234 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1235 var scrollTop
, body
;
1237 if ( OO
.ui
.scrollableElement
=== undefined ) {
1238 body
= el
.ownerDocument
.body
;
1239 scrollTop
= body
.scrollTop
;
1242 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1243 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1244 if ( Math
.round( body
.scrollTop
) === 1 ) {
1245 body
.scrollTop
= scrollTop
;
1246 OO
.ui
.scrollableElement
= 'body';
1248 OO
.ui
.scrollableElement
= 'documentElement';
1252 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1256 * Get closest scrollable container.
1258 * Traverses up until either a scrollable element or the root is reached, in which case the root
1259 * scrollable element will be returned (see #getRootScrollableElement).
1262 * @param {HTMLElement} el Element to find scrollable container for
1263 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1264 * @return {HTMLElement} Closest scrollable container
1266 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1268 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1269 // 'overflow-y' have different values, so we need to check the separate properties.
1270 props
= [ 'overflow-x', 'overflow-y' ],
1271 $parent
= $( el
).parent();
1273 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1274 props
= [ 'overflow-' + dimension
];
1277 // Special case for the document root (which doesn't really have any scrollable container,
1278 // since it is the ultimate scrollable container, but this is probably saner than null or
1280 if ( $( el
).is( 'html, body' ) ) {
1281 return this.getRootScrollableElement( el
);
1284 while ( $parent
.length
) {
1285 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1286 return $parent
[ 0 ];
1290 val
= $parent
.css( props
[ i
] );
1291 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will
1292 // never be scrolled in that direction, but they can actually be scrolled
1293 // programatically. The user can unintentionally perform a scroll in such case even if
1294 // the application doesn't scroll programatically, e.g. when jumping to an anchor, or
1295 // when using built-in find functionality.
1296 // This could cause funny issues...
1297 if ( val
=== 'auto' || val
=== 'scroll' ) {
1298 return $parent
[ 0 ];
1301 $parent
= $parent
.parent();
1303 // The element is unattached... return something mostly sane
1304 return this.getRootScrollableElement( el
);
1308 * Scroll element into view.
1311 * @param {HTMLElement|Object} elOrPosition Element to scroll into view
1312 * @param {Object} [config] Configuration options
1313 * @param {string} [config.animate=true] Animate to the new scroll offset.
1314 * @param {string} [config.duration='fast'] jQuery animation duration value
1315 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1316 * to scroll in both directions
1317 * @param {Object} [config.padding] Additional padding on the container to scroll past.
1318 * Object containing any of 'top', 'bottom', 'left', or 'right' as numbers.
1319 * @param {Object} [config.scrollContainer] Scroll container. Defaults to
1320 * getClosestScrollableContainer of the element.
1321 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1323 OO
.ui
.Element
.static.scrollIntoView = function ( elOrPosition
, config
) {
1324 var position
, animations
, container
, $container
, elementPosition
, containerDimensions
,
1325 $window
, padding
, animate
, method
,
1326 deferred
= $.Deferred();
1328 // Configuration initialization
1329 config
= config
|| {};
1331 padding
= $.extend( {
1336 }, config
.padding
);
1338 animate
= config
.animate
!== false;
1341 elementPosition
= elOrPosition
instanceof HTMLElement
?
1342 this.getDimensions( elOrPosition
).rect
:
1344 container
= config
.scrollContainer
|| (
1345 elOrPosition
instanceof HTMLElement
?
1346 this.getClosestScrollableContainer( elOrPosition
, config
.direction
) :
1347 // No scrollContainer or element
1348 this.getClosestScrollableContainer( document
.body
)
1350 $container
= $( container
);
1351 containerDimensions
= this.getDimensions( container
);
1352 $window
= $( this.getWindow( container
) );
1354 // Compute the element's position relative to the container
1355 if ( $container
.is( 'html, body' ) ) {
1356 // If the scrollable container is the root, this is easy
1358 top
: elementPosition
.top
,
1359 bottom
: $window
.innerHeight() - elementPosition
.bottom
,
1360 left
: elementPosition
.left
,
1361 right
: $window
.innerWidth() - elementPosition
.right
1364 // Otherwise, we have to subtract el's coordinates from container's coordinates
1366 top
: elementPosition
.top
-
1367 ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1368 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
-
1369 containerDimensions
.scrollbar
.bottom
- elementPosition
.bottom
,
1370 left
: elementPosition
.left
-
1371 ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1372 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
-
1373 containerDimensions
.scrollbar
.right
- elementPosition
.right
1377 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1378 if ( position
.top
< padding
.top
) {
1379 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
- padding
.top
;
1380 } else if ( position
.bottom
< padding
.bottom
) {
1381 animations
.scrollTop
= containerDimensions
.scroll
.top
+
1382 // Scroll the bottom into view, but not at the expense
1383 // of scrolling the top out of view
1384 Math
.min( position
.top
- padding
.top
, -position
.bottom
+ padding
.bottom
);
1387 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1388 if ( position
.left
< padding
.left
) {
1389 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
- padding
.left
;
1390 } else if ( position
.right
< padding
.right
) {
1391 animations
.scrollLeft
= containerDimensions
.scroll
.left
+
1392 // Scroll the right into view, but not at the expense
1393 // of scrolling the left out of view
1394 Math
.min( position
.left
- padding
.left
, -position
.right
+ padding
.right
);
1396 if ( animations
.scrollLeft
!== undefined ) {
1397 animations
.scrollLeft
= OO
.ui
.Element
.static.computeNativeScrollLeft( animations
.scrollLeft
, container
);
1400 if ( !$.isEmptyObject( animations
) ) {
1402 // eslint-disable-next-line no-jquery/no-animate
1403 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1404 $container
.queue( function ( next
) {
1409 $container
.stop( true );
1410 for ( method
in animations
) {
1411 $container
[ method
]( animations
[ method
] );
1418 return deferred
.promise();
1422 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1423 * and reserve space for them, because it probably doesn't.
1425 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1426 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1427 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a
1428 * reflow, and then reattach (or show) them back.
1431 * @param {HTMLElement} el Element to reconsider the scrollbars on
1433 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1434 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1435 // Save scroll position
1436 scrollLeft
= el
.scrollLeft
;
1437 scrollTop
= el
.scrollTop
;
1438 // Detach all children
1439 while ( el
.firstChild
) {
1440 nodes
.push( el
.firstChild
);
1441 el
.removeChild( el
.firstChild
);
1444 // eslint-disable-next-line no-void
1445 void el
.offsetHeight
;
1446 // Reattach all children
1447 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1448 el
.appendChild( nodes
[ i
] );
1450 // Restore scroll position (no-op if scrollbars disappeared)
1451 el
.scrollLeft
= scrollLeft
;
1452 el
.scrollTop
= scrollTop
;
1458 * Toggle visibility of an element.
1460 * @param {boolean} [show] Make element visible, omit to toggle visibility
1463 * @return {OO.ui.Element} The element, for chaining
1465 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1466 show
= show
=== undefined ? !this.visible
: !!show
;
1468 if ( show
!== this.isVisible() ) {
1469 this.visible
= show
;
1470 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1471 this.emit( 'toggle', show
);
1478 * Check if element is visible.
1480 * @return {boolean} element is visible
1482 OO
.ui
.Element
.prototype.isVisible = function () {
1483 return this.visible
;
1489 * @return {Mixed} Element data
1491 OO
.ui
.Element
.prototype.getData = function () {
1498 * @param {Mixed} data Element data
1500 * @return {OO.ui.Element} The element, for chaining
1502 OO
.ui
.Element
.prototype.setData = function ( data
) {
1508 * Set the element has an 'id' attribute.
1510 * @param {string} id
1512 * @return {OO.ui.Element} The element, for chaining
1514 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1515 this.elementId
= id
;
1516 this.$element
.attr( 'id', id
);
1521 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1522 * and return its value.
1526 OO
.ui
.Element
.prototype.getElementId = function () {
1527 if ( this.elementId
=== null ) {
1528 this.setElementId( OO
.ui
.generateElementId() );
1530 return this.elementId
;
1534 * Check if element supports one or more methods.
1536 * @param {string|string[]} methods Method or list of methods to check
1537 * @return {boolean} All methods are supported
1539 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1543 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1544 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1545 if ( typeof this[ methods
[ i
] ] === 'function' ) {
1550 return methods
.length
=== support
;
1554 * Update the theme-provided classes.
1556 * @localdoc This is called in element mixins and widget classes any time state changes.
1557 * Updating is debounced, minimizing overhead of changing multiple attributes and
1558 * guaranteeing that theme updates do not occur within an element's constructor
1560 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1561 OO
.ui
.theme
.queueUpdateElementClasses( this );
1565 * Get the HTML tag name.
1567 * Override this method to base the result on instance information.
1569 * @return {string} HTML tag name
1571 OO
.ui
.Element
.prototype.getTagName = function () {
1572 return this.constructor.static.tagName
;
1576 * Check if the element is attached to the DOM
1578 * @return {boolean} The element is attached to the DOM
1580 OO
.ui
.Element
.prototype.isElementAttached = function () {
1581 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1585 * Get the DOM document.
1587 * @return {HTMLDocument} Document object
1589 OO
.ui
.Element
.prototype.getElementDocument = function () {
1590 // Don't cache this in other ways either because subclasses could can change this.$element
1591 return OO
.ui
.Element
.static.getDocument( this.$element
);
1595 * Get the DOM window.
1597 * @return {Window} Window object
1599 OO
.ui
.Element
.prototype.getElementWindow = function () {
1600 return OO
.ui
.Element
.static.getWindow( this.$element
);
1604 * Get closest scrollable container.
1606 * @return {HTMLElement} Closest scrollable container
1608 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1609 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1613 * Get group element is in.
1615 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1617 OO
.ui
.Element
.prototype.getElementGroup = function () {
1618 return this.elementGroup
;
1622 * Set group element is in.
1624 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1626 * @return {OO.ui.Element} The element, for chaining
1628 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1629 this.elementGroup
= group
;
1634 * Scroll element into view.
1636 * @param {Object} [config] Configuration options
1637 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1639 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1641 !this.isElementAttached() ||
1642 !this.isVisible() ||
1643 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1645 return $.Deferred().resolve();
1647 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1651 * Restore the pre-infusion dynamic state for this widget.
1653 * This method is called after #$element has been inserted into DOM. The parameter is the return
1654 * value of #gatherPreInfuseState.
1657 * @param {Object} state
1659 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1663 * Wraps an HTML snippet for use with configuration values which default
1664 * to strings. This bypasses the default html-escaping done to string
1670 * @param {string} [content] HTML content
1672 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1674 this.content
= content
;
1679 OO
.initClass( OO
.ui
.HtmlSnippet
);
1686 * @return {string} Unchanged HTML snippet.
1688 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1689 return this.content
;
1693 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in
1694 * a way that is centrally controlled and can be updated dynamically. Layouts can be, and usually
1696 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout},
1697 * {@link OO.ui.FormLayout FormLayout}, {@link OO.ui.PanelLayout PanelLayout},
1698 * {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1699 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout}
1700 * for more information and examples.
1704 * @extends OO.ui.Element
1705 * @mixins OO.EventEmitter
1708 * @param {Object} [config] Configuration options
1710 OO
.ui
.Layout
= function OoUiLayout( config
) {
1711 // Configuration initialization
1712 config
= config
|| {};
1714 // Parent constructor
1715 OO
.ui
.Layout
.parent
.call( this, config
);
1717 // Mixin constructors
1718 OO
.EventEmitter
.call( this );
1721 this.$element
.addClass( 'oo-ui-layout' );
1726 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1727 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1732 * Reset scroll offsets
1735 * @return {OO.ui.Layout} The layout, for chaining
1737 OO
.ui
.Layout
.prototype.resetScroll = function () {
1738 this.$element
[ 0 ].scrollTop
= 0;
1739 OO
.ui
.Element
.static.setScrollLeft( this.$element
[ 0 ], 0 );
1745 * Widgets are compositions of one or more OOUI elements that users can both view
1746 * and interact with. All widgets can be configured and modified via a standard API,
1747 * and their state can change dynamically according to a model.
1751 * @extends OO.ui.Element
1752 * @mixins OO.EventEmitter
1755 * @param {Object} [config] Configuration options
1756 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1757 * appearance reflects this state.
1759 OO
.ui
.Widget
= function OoUiWidget( config
) {
1760 // Initialize config
1761 config
= $.extend( { disabled
: false }, config
);
1763 // Parent constructor
1764 OO
.ui
.Widget
.parent
.call( this, config
);
1766 // Mixin constructors
1767 OO
.EventEmitter
.call( this );
1770 this.disabled
= null;
1771 this.wasDisabled
= null;
1774 this.$element
.addClass( 'oo-ui-widget' );
1775 this.setDisabled( !!config
.disabled
);
1780 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1781 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1788 * A 'disable' event is emitted when the disabled state of the widget changes
1789 * (i.e. on disable **and** enable).
1791 * @param {boolean} disabled Widget is disabled
1797 * A 'toggle' event is emitted when the visibility of the widget changes.
1799 * @param {boolean} visible Widget is visible
1805 * Check if the widget is disabled.
1807 * @return {boolean} Widget is disabled
1809 OO
.ui
.Widget
.prototype.isDisabled = function () {
1810 return this.disabled
;
1814 * Set the 'disabled' state of the widget.
1816 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1818 * @param {boolean} disabled Disable widget
1820 * @return {OO.ui.Widget} The widget, for chaining
1822 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1825 this.disabled
= !!disabled
;
1826 isDisabled
= this.isDisabled();
1827 if ( isDisabled
!== this.wasDisabled
) {
1828 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1829 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1830 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1831 this.emit( 'disable', isDisabled
);
1832 this.updateThemeClasses();
1834 this.wasDisabled
= isDisabled
;
1840 * Update the disabled state, in case of changes in parent widget.
1843 * @return {OO.ui.Widget} The widget, for chaining
1845 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1846 this.setDisabled( this.disabled
);
1851 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1854 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1857 * @return {string|null} The ID of the labelable element
1859 OO
.ui
.Widget
.prototype.getInputId = function () {
1864 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1865 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1866 * override this method to provide intuitive, accessible behavior.
1868 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1869 * Individual widgets may override it too.
1871 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1874 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1885 OO
.ui
.Theme
= function OoUiTheme() {
1886 this.elementClassesQueue
= [];
1887 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1892 OO
.initClass( OO
.ui
.Theme
);
1897 * Get a list of classes to be applied to a widget.
1899 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1900 * otherwise state transitions will not work properly.
1902 * @param {OO.ui.Element} element Element for which to get classes
1903 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1905 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1906 return { on
: [], off
: [] };
1910 * Update CSS classes provided by the theme.
1912 * For elements with theme logic hooks, this should be called any time there's a state change.
1914 * @param {OO.ui.Element} element Element for which to update classes
1916 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1917 var $elements
= $( [] ),
1918 classes
= this.getElementClasses( element
);
1920 if ( element
.$icon
) {
1921 $elements
= $elements
.add( element
.$icon
);
1923 if ( element
.$indicator
) {
1924 $elements
= $elements
.add( element
.$indicator
);
1928 .removeClass( classes
.off
)
1929 .addClass( classes
.on
);
1935 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1937 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1938 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1941 this.elementClassesQueue
= [];
1945 * Queue #updateElementClasses to be called for this element.
1947 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1948 * to make them synchronous.
1950 * @param {OO.ui.Element} element Element for which to update classes
1952 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1953 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1954 // the most common case (this method is often called repeatedly for the same element).
1955 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1958 this.elementClassesQueue
.push( element
);
1959 this.debouncedUpdateQueuedElementClasses();
1963 * Get the transition duration in milliseconds for dialogs opening/closing
1965 * The dialog should be fully rendered this many milliseconds after the
1966 * ready process has executed.
1968 * @return {number} Transition duration in milliseconds
1970 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1975 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1976 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1977 * order in which users will navigate through the focusable elements via the Tab key.
1980 * // TabIndexedElement is mixed into the ButtonWidget class
1981 * // to provide a tabIndex property.
1982 * var button1 = new OO.ui.ButtonWidget( {
1986 * button2 = new OO.ui.ButtonWidget( {
1990 * button3 = new OO.ui.ButtonWidget( {
1994 * button4 = new OO.ui.ButtonWidget( {
1998 * $( document.body ).append(
2009 * @param {Object} [config] Configuration options
2010 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
2011 * the functionality is applied to the element created by the class ($element). If a different
2012 * element is specified, the tabindex functionality will be applied to it instead.
2013 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the
2014 * tab-navigation order (e.g., 1 for the first focusable element). Use 0 to use the default
2015 * navigation order; use -1 to remove the element from the tab-navigation flow.
2017 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
2018 // Configuration initialization
2019 config
= $.extend( { tabIndex
: 0 }, config
);
2022 this.$tabIndexed
= null;
2023 this.tabIndex
= null;
2026 this.connect( this, {
2027 disable
: 'onTabIndexedElementDisable'
2031 this.setTabIndex( config
.tabIndex
);
2032 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
2037 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
2042 * Set the element that should use the tabindex functionality.
2044 * This method is used to retarget a tabindex mixin so that its functionality applies
2045 * to the specified element. If an element is currently using the functionality, the mixin’s
2046 * effect on that element is removed before the new element is set up.
2048 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
2050 * @return {OO.ui.Element} The element, for chaining
2052 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
2053 var tabIndex
= this.tabIndex
;
2054 // Remove attributes from old $tabIndexed
2055 this.setTabIndex( null );
2056 // Force update of new $tabIndexed
2057 this.$tabIndexed
= $tabIndexed
;
2058 this.tabIndex
= tabIndex
;
2059 return this.updateTabIndex();
2063 * Set the value of the tabindex.
2065 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
2067 * @return {OO.ui.Element} The element, for chaining
2069 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
2070 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
2072 if ( this.tabIndex
!== tabIndex
) {
2073 this.tabIndex
= tabIndex
;
2074 this.updateTabIndex();
2081 * Update the `tabindex` attribute, in case of changes to tab index or
2086 * @return {OO.ui.Element} The element, for chaining
2088 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
2089 if ( this.$tabIndexed
) {
2090 if ( this.tabIndex
!== null ) {
2091 // Do not index over disabled elements
2092 this.$tabIndexed
.attr( {
2093 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
2094 // Support: ChromeVox and NVDA
2095 // These do not seem to inherit aria-disabled from parent elements
2096 'aria-disabled': this.isDisabled().toString()
2099 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
2106 * Handle disable events.
2109 * @param {boolean} disabled Element is disabled
2111 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
2112 this.updateTabIndex();
2116 * Get the value of the tabindex.
2118 * @return {number|null} Tabindex value
2120 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2121 return this.tabIndex
;
2125 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2127 * If the element already has an ID then that is returned, otherwise unique ID is
2128 * generated, set on the element, and returned.
2130 * @return {string|null} The ID of the focusable element
2132 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2135 if ( !this.$tabIndexed
) {
2138 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2142 id
= this.$tabIndexed
.attr( 'id' );
2143 if ( id
=== undefined ) {
2144 id
= OO
.ui
.generateElementId();
2145 this.$tabIndexed
.attr( 'id', id
);
2152 * Whether the node is 'labelable' according to the HTML spec
2153 * (i.e., whether it can be interacted with through a `<label for="…">`).
2154 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2157 * @param {jQuery} $node
2160 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2162 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2163 tagName
= ( $node
.prop( 'tagName' ) || '' ).toLowerCase();
2165 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2168 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2175 * Focus this element.
2178 * @return {OO.ui.Element} The element, for chaining
2180 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2181 if ( !this.isDisabled() ) {
2182 this.$tabIndexed
.trigger( 'focus' );
2188 * Blur this element.
2191 * @return {OO.ui.Element} The element, for chaining
2193 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2194 this.$tabIndexed
.trigger( 'blur' );
2199 * @inheritdoc OO.ui.Widget
2201 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2206 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2207 * interface element that can be configured with access keys for keyboard interaction.
2208 * See the [OOUI documentation on MediaWiki] [1] for examples.
2210 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2216 * @param {Object} [config] Configuration options
2217 * @cfg {jQuery} [$button] The button element created by the class.
2218 * If this configuration is omitted, the button element will use a generated `<a>`.
2219 * @cfg {boolean} [framed=true] Render the button with a frame
2221 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2222 // Configuration initialization
2223 config
= config
|| {};
2226 this.$button
= null;
2228 this.active
= config
.active
!== undefined && config
.active
;
2229 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
2230 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2231 this.onDocumentKeyUpHandler
= this.onDocumentKeyUp
.bind( this );
2232 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2233 this.onClickHandler
= this.onClick
.bind( this );
2234 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2237 this.$element
.addClass( 'oo-ui-buttonElement' );
2238 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2239 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2244 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2246 /* Static Properties */
2249 * Cancel mouse down events.
2251 * This property is usually set to `true` to prevent the focus from changing when the button is
2253 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and
2254 * {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} use a value of `false` so that dragging
2255 * behavior is possible and mousedown events can be handled by a parent widget.
2259 * @property {boolean}
2261 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2266 * A 'click' event is emitted when the button element is clicked.
2274 * Set the button element.
2276 * This method is used to retarget a button mixin so that its functionality applies to
2277 * the specified button element instead of the one created by the class. If a button element
2278 * is already set, the method will remove the mixin’s effect on that element.
2280 * @param {jQuery} $button Element to use as button
2282 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2283 if ( this.$button
) {
2285 .removeClass( 'oo-ui-buttonElement-button' )
2286 .removeAttr( 'role accesskey' )
2288 mousedown
: this.onMouseDownHandler
,
2289 keydown
: this.onKeyDownHandler
,
2290 click
: this.onClickHandler
,
2291 keypress
: this.onKeyPressHandler
2295 this.$button
= $button
2296 .addClass( 'oo-ui-buttonElement-button' )
2298 mousedown
: this.onMouseDownHandler
,
2299 keydown
: this.onKeyDownHandler
,
2300 click
: this.onClickHandler
,
2301 keypress
: this.onKeyPressHandler
2304 // Add `role="button"` on `<a>` elements, where it's needed
2305 // `toUpperCase()` is added for XHTML documents
2306 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2307 this.$button
.attr( 'role', 'button' );
2312 * Handles mouse down events.
2315 * @param {jQuery.Event} e Mouse down event
2316 * @return {undefined|boolean} False to prevent default if event is handled
2318 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2319 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2322 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2323 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2324 // reliably remove the pressed class
2325 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2326 // Prevent change of focus unless specifically configured otherwise
2327 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2333 * Handles document mouse up events.
2336 * @param {MouseEvent} e Mouse up event
2338 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentMouseUp = function ( e
) {
2339 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2342 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2343 // Stop listening for mouseup, since we only needed this once
2344 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2348 * Handles mouse click events.
2351 * @param {jQuery.Event} e Mouse click event
2353 * @return {undefined|boolean} False to prevent default if event is handled
2355 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2356 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2357 if ( this.emit( 'click' ) ) {
2364 * Handles key down events.
2367 * @param {jQuery.Event} e Key down event
2369 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2370 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2373 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2374 // Run the keyup handler no matter where the key is when the button is let go, so we can
2375 // reliably remove the pressed class
2376 this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2380 * Handles document key up events.
2383 * @param {KeyboardEvent} e Key up event
2385 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentKeyUp = function ( e
) {
2386 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2389 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2390 // Stop listening for keyup, since we only needed this once
2391 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2395 * Handles key press events.
2398 * @param {jQuery.Event} e Key press event
2400 * @return {undefined|boolean} False to prevent default if event is handled
2402 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2403 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2404 if ( this.emit( 'click' ) ) {
2411 * Check if button has a frame.
2413 * @return {boolean} Button is framed
2415 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2420 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame
2423 * @param {boolean} [framed] Make button framed, omit to toggle
2425 * @return {OO.ui.Element} The element, for chaining
2427 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2428 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2429 if ( framed
!== this.framed
) {
2430 this.framed
= framed
;
2432 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2433 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2434 this.updateThemeClasses();
2441 * Set the button's active state.
2443 * The active state can be set on:
2445 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2446 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2447 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2450 * @param {boolean} value Make button active
2452 * @return {OO.ui.Element} The element, for chaining
2454 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2455 this.active
= !!value
;
2456 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2457 this.updateThemeClasses();
2462 * Check if the button is active
2465 * @return {boolean} The button is active
2467 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2472 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2473 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2474 * items from the group is done through the interface the class provides.
2475 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2477 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2480 * @mixins OO.EmitterList
2484 * @param {Object} [config] Configuration options
2485 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2486 * is omitted, the group element will use a generated `<div>`.
2488 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2489 // Configuration initialization
2490 config
= config
|| {};
2492 // Mixin constructors
2493 OO
.EmitterList
.call( this, config
);
2499 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2504 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2511 * A change event is emitted when the set of selected items changes.
2513 * @param {OO.ui.Element[]} items Items currently in the group
2519 * Set the group element.
2521 * If an element is already set, items will be moved to the new element.
2523 * @param {jQuery} $group Element to use as group
2525 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2528 this.$group
= $group
;
2529 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2530 this.$group
.append( this.items
[ i
].$element
);
2535 * Find an item by its data.
2537 * Only the first item with matching data will be returned. To return all matching items,
2538 * use the #findItemsFromData method.
2540 * @param {Object} data Item data to search for
2541 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2543 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2545 hash
= OO
.getHash( data
);
2547 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2548 item
= this.items
[ i
];
2549 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2558 * Find items by their data.
2560 * All items with matching data will be returned. To return only the first match, use the
2561 * #findItemFromData method instead.
2563 * @param {Object} data Item data to search for
2564 * @return {OO.ui.Element[]} Items with equivalent data
2566 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2568 hash
= OO
.getHash( data
),
2571 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2572 item
= this.items
[ i
];
2573 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2582 * Add items to the group.
2584 * Items will be added to the end of the group array unless the optional `index` parameter
2585 * specifies a different insertion point. Adding an existing item will move it to the end of the
2586 * array or the point specified by the `index`.
2588 * @param {OO.ui.Element[]} items An array of items to add to the group
2589 * @param {number} [index] Index of the insertion point
2591 * @return {OO.ui.Element} The element, for chaining
2593 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2595 if ( items
.length
=== 0 ) {
2600 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2602 this.emit( 'change', this.getItems() );
2607 * Move an item from its current position to a new index.
2609 * The item is expected to exist in the list. If it doesn't,
2610 * the method will throw an exception.
2612 * See https://doc.wikimedia.org/oojs/master/OO.EmitterList.html
2615 * @param {OO.EventEmitter} items Item to add
2616 * @param {number} newIndex Index to move the item to
2617 * @return {number} The index the item was moved to
2618 * @throws {Error} If item is not in the list
2620 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2621 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2622 this.insertItemElements( items
, newIndex
);
2625 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2631 * Utility method to insert an item into the list, and
2632 * connect it to aggregate events.
2634 * Don't call this directly unless you know what you're doing.
2635 * Use #addItems instead.
2637 * This method can be extended in child classes to produce
2638 * different behavior when an item is inserted. For example,
2639 * inserted items may also be attached to the DOM or may
2640 * interact with some other nodes in certain ways. Extending
2641 * this method is allowed, but if overridden, the aggregation
2642 * of events must be preserved, or behavior of emitted events
2645 * If you are extending this method, please make sure the
2646 * parent method is called.
2648 * See https://doc.wikimedia.org/oojs/master/OO.EmitterList.html
2651 * @param {OO.EventEmitter|Object} item Item to add
2652 * @param {number} index Index to add items at
2653 * @return {number} The index the item was added at
2655 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2656 item
.setElementGroup( this );
2657 this.insertItemElements( item
, index
);
2660 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2666 * Insert elements into the group
2669 * @param {OO.ui.Element} itemWidget Item to insert
2670 * @param {number} index Insertion index
2672 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2673 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2674 this.$group
.append( itemWidget
.$element
);
2675 } else if ( index
=== 0 ) {
2676 this.$group
.prepend( itemWidget
.$element
);
2678 this.items
[ index
].$element
.before( itemWidget
.$element
);
2683 * Remove the specified items from a group.
2685 * Removed items are detached (not removed) from the DOM so that they may be reused.
2686 * To remove all items from a group, you may wish to use the #clearItems method instead.
2688 * @param {OO.ui.Element[]} items An array of items to remove
2690 * @return {OO.ui.Element} The element, for chaining
2692 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2693 var i
, len
, item
, index
;
2695 if ( items
.length
=== 0 ) {
2699 // Remove specific items elements
2700 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2702 index
= this.items
.indexOf( item
);
2703 if ( index
!== -1 ) {
2704 item
.setElementGroup( null );
2705 item
.$element
.detach();
2710 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2712 this.emit( 'change', this.getItems() );
2717 * Clear all items from the group.
2719 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2720 * To remove only a subset of items from a group, use the #removeItems method.
2723 * @return {OO.ui.Element} The element, for chaining
2725 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2728 // Remove all item elements
2729 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2730 this.items
[ i
].setElementGroup( null );
2731 this.items
[ i
].$element
.detach();
2735 OO
.EmitterList
.prototype.clearItems
.call( this );
2737 this.emit( 'change', this.getItems() );
2742 * LabelElement is often mixed into other classes to generate a label, which
2743 * helps identify the function of an interface element.
2744 * See the [OOUI documentation on MediaWiki] [1] for more information.
2746 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2752 * @param {Object} [config] Configuration options
2753 * @cfg {jQuery} [$label] The label element created by the class. If this
2754 * configuration is omitted, the label element will use a generated `<span>`.
2755 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be
2756 * specified as a plaintext string, a jQuery selection of elements, or a function that will
2757 * produce a string in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2758 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2759 * @cfg {boolean} [invisibleLabel] Whether the label should be visually hidden (but still
2760 * accessible to screen-readers).
2762 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2763 // Configuration initialization
2764 config
= config
|| {};
2769 this.invisibleLabel
= null;
2772 this.setLabel( config
.label
|| this.constructor.static.label
);
2773 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2774 this.setInvisibleLabel( config
.invisibleLabel
);
2779 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2784 * @event labelChange
2785 * @param {string} value
2788 /* Static Properties */
2791 * The label text. The label can be specified as a plaintext string, a function that will
2792 * produce a string in the future, or `null` for no label. The static value will
2793 * be overridden if a label is specified with the #label config option.
2797 * @property {string|Function|null}
2799 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2801 /* Static methods */
2804 * Highlight the first occurrence of the query in the given text
2806 * @param {string} text Text
2807 * @param {string} query Query to find
2808 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2809 * @param {boolean} [combineMarks=false] Pull combining marks into highlighted text
2810 * @return {jQuery} Text with the first match of the query
2811 * sub-string wrapped in highlighted span
2813 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
, combineMarks
) {
2816 $result
= $( '<span>' ),
2824 qLen
= query
.length
;
2825 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2826 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2831 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2834 if ( !query
.length
|| offset
=== -1 ) {
2835 $result
.text( text
);
2837 // Look for combining characters after the match
2838 if ( combineMarks
) {
2839 // Equivalent to \p{Mark} (which is not currently available in JavaScript)
2840 comboMarks
= '[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]';
2842 comboRegex
= new RegExp( '(^)' + comboMarks
+ '*' );
2843 comboMatch
= text
.slice( offset
+ query
.length
).match( comboRegex
);
2845 if ( comboMatch
&& comboMatch
.length
) {
2846 comboLength
= comboMatch
[ 0 ].length
;
2851 document
.createTextNode( text
.slice( 0, offset
) ),
2853 .addClass( 'oo-ui-labelElement-label-highlight' )
2854 .text( text
.slice( offset
, offset
+ query
.length
+ comboLength
) ),
2855 document
.createTextNode( text
.slice( offset
+ query
.length
+ comboLength
) )
2858 return $result
.contents();
2864 * Set the label element.
2866 * If an element is already set, it will be cleaned up before setting up the new element.
2868 * @param {jQuery} $label Element to use as label
2870 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2871 if ( this.$label
) {
2872 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2875 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2876 this.setLabelContent( this.label
);
2882 * An empty string will result in the label being hidden. A string containing only whitespace will
2883 * be converted to a single ` `.
2885 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that
2886 * returns nodes or text; or null for no label
2888 * @return {OO.ui.Element} The element, for chaining
2890 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2891 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2892 label
= ( ( typeof label
=== 'string' || label
instanceof $ ) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2894 if ( this.label
!== label
) {
2895 if ( this.$label
) {
2896 this.setLabelContent( label
);
2899 this.emit( 'labelChange' );
2902 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2908 * Set whether the label should be visually hidden (but still accessible to screen-readers).
2910 * @param {boolean} invisibleLabel
2912 * @return {OO.ui.Element} The element, for chaining
2914 OO
.ui
.mixin
.LabelElement
.prototype.setInvisibleLabel = function ( invisibleLabel
) {
2915 invisibleLabel
= !!invisibleLabel
;
2917 if ( this.invisibleLabel
!== invisibleLabel
) {
2918 this.invisibleLabel
= invisibleLabel
;
2919 this.emit( 'labelChange' );
2922 this.$label
.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel
);
2923 // Pretend that there is no label, a lot of CSS has been written with this assumption
2924 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2930 * Set the label as plain text with a highlighted query
2932 * @param {string} text Text label to set
2933 * @param {string} query Substring of text to highlight
2934 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2935 * @param {boolean} [combineMarks=false] Pull combining marks into highlighted text
2937 * @return {OO.ui.Element} The element, for chaining
2939 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function (
2940 text
, query
, compare
, combineMarks
2942 return this.setLabel(
2943 this.constructor.static.highlightQuery( text
, query
, compare
, combineMarks
)
2950 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2951 * text; or null for no label
2953 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2958 * Set the content of the label.
2960 * Do not call this method until after the label element has been set by #setLabelElement.
2963 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2964 * text; or null for no label
2966 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2967 if ( typeof label
=== 'string' ) {
2968 if ( label
.match( /^\s*$/ ) ) {
2969 // Convert whitespace only string to a single non-breaking space
2970 this.$label
.html( ' ' );
2972 this.$label
.text( label
);
2974 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2975 this.$label
.html( label
.toString() );
2976 } else if ( label
instanceof $ ) {
2977 this.$label
.empty().append( label
);
2979 this.$label
.empty();
2984 * IconElement is often mixed into other classes to generate an icon.
2985 * Icons are graphics, about the size of normal text. They are used to aid the user
2986 * in locating a control or to convey information in a space-efficient way. See the
2987 * [OOUI documentation on MediaWiki] [1] for a list of icons
2988 * included in the library.
2990 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2996 * @param {Object} [config] Configuration options
2997 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2998 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2999 * the icon element be set to an existing icon instead of the one generated by this class, set a
3000 * value using a jQuery selection. For example:
3002 * // Use a <div> tag instead of a <span>
3003 * $icon: $( '<div>' )
3004 * // Use an existing icon element instead of the one generated by the class
3005 * $icon: this.$element
3006 * // Use an icon element from a child widget
3007 * $icon: this.childwidget.$element
3008 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a
3009 * map of symbolic names. A map is used for i18n purposes and contains a `default` icon
3010 * name and additional names keyed by language code. The `default` name is used when no icon is
3011 * keyed by the user's language.
3013 * Example of an i18n map:
3015 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
3016 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
3017 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
3019 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
3020 // Configuration initialization
3021 config
= config
|| {};
3028 this.setIcon( config
.icon
|| this.constructor.static.icon
);
3029 this.setIconElement( config
.$icon
|| $( '<span>' ) );
3034 OO
.initClass( OO
.ui
.mixin
.IconElement
);
3036 /* Static Properties */
3039 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map
3040 * is used for i18n purposes and contains a `default` icon name and additional names keyed by
3041 * language code. The `default` name is used when no icon is keyed by the user's language.
3043 * Example of an i18n map:
3045 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
3047 * Note: the static property will be overridden if the #icon configuration is used.
3051 * @property {Object|string}
3053 OO
.ui
.mixin
.IconElement
.static.icon
= null;
3056 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
3057 * function that returns title text, or `null` for no title.
3059 * The static property will be overridden if the #iconTitle configuration is used.
3063 * @property {string|Function|null}
3065 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
3070 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
3071 * applies to the specified icon element instead of the one created by the class. If an icon
3072 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
3073 * and mixin methods will no longer affect the element.
3075 * @param {jQuery} $icon Element to use as icon
3077 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
3080 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
3081 .removeAttr( 'title' );
3085 .addClass( 'oo-ui-iconElement-icon' )
3086 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
3087 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
3088 if ( this.iconTitle
!== null ) {
3089 this.$icon
.attr( 'title', this.iconTitle
);
3092 this.updateThemeClasses();
3096 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
3097 * The icon parameter can also be set to a map of icon names. See the #icon config setting
3100 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
3101 * by language code, or `null` to remove the icon.
3103 * @return {OO.ui.Element} The element, for chaining
3105 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
3106 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
3107 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
3109 if ( this.icon
!== icon
) {
3111 if ( this.icon
!== null ) {
3112 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
3114 if ( icon
!== null ) {
3115 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
3121 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
3123 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
3125 this.updateThemeClasses();
3131 * Get the symbolic name of the icon.
3133 * @return {string} Icon name
3135 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
3140 * IndicatorElement is often mixed into other classes to generate an indicator.
3141 * Indicators are small graphics that are generally used in two ways:
3143 * - To draw attention to the status of an item. For example, an indicator might be
3144 * used to show that an item in a list has errors that need to be resolved.
3145 * - To clarify the function of a control that acts in an exceptional way (a button
3146 * that opens a menu instead of performing an action directly, for example).
3148 * For a list of indicators included in the library, please see the
3149 * [OOUI documentation on MediaWiki] [1].
3151 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3157 * @param {Object} [config] Configuration options
3158 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
3159 * configuration is omitted, the indicator element will use a generated `<span>`.
3160 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3161 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3163 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3165 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
3166 // Configuration initialization
3167 config
= config
|| {};
3170 this.$indicator
= null;
3171 this.indicator
= null;
3174 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
3175 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
3180 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
3182 /* Static Properties */
3185 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3186 * The static property will be overridden if the #indicator configuration is used.
3190 * @property {string|null}
3192 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
3195 * A text string used as the indicator title, a function that returns title text, or `null`
3196 * for no title. The static property will be overridden if the #indicatorTitle configuration is
3201 * @property {string|Function|null}
3203 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
3208 * Set the indicator element.
3210 * If an element is already set, it will be cleaned up before setting up the new element.
3212 * @param {jQuery} $indicator Element to use as indicator
3214 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
3215 if ( this.$indicator
) {
3217 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
3218 .removeAttr( 'title' );
3221 this.$indicator
= $indicator
3222 .addClass( 'oo-ui-indicatorElement-indicator' )
3223 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
3224 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
3225 if ( this.indicatorTitle
!== null ) {
3226 this.$indicator
.attr( 'title', this.indicatorTitle
);
3229 this.updateThemeClasses();
3233 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null`
3234 * to remove the indicator.
3236 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3238 * @return {OO.ui.Element} The element, for chaining
3240 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
3241 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
3243 if ( this.indicator
!== indicator
) {
3244 if ( this.$indicator
) {
3245 if ( this.indicator
!== null ) {
3246 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
3248 if ( indicator
!== null ) {
3249 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
3252 this.indicator
= indicator
;
3255 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
3256 if ( this.$indicator
) {
3257 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
3259 this.updateThemeClasses();
3265 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3267 * @return {string} Symbolic name of indicator
3269 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
3270 return this.indicator
;
3274 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3275 * additional functionality to an element created by another class. The class provides
3276 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3277 * which are used to customize the look and feel of a widget to better describe its
3278 * importance and functionality.
3280 * The library currently contains the following styling flags for general use:
3282 * - **progressive**: Progressive styling is applied to convey that the widget will move the user
3283 * forward in a process.
3284 * - **destructive**: Destructive styling is applied to convey that the widget will remove
3287 * The flags affect the appearance of the buttons:
3290 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3291 * var button1 = new OO.ui.ButtonWidget( {
3292 * label: 'Progressive',
3293 * flags: 'progressive'
3295 * button2 = new OO.ui.ButtonWidget( {
3296 * label: 'Destructive',
3297 * flags: 'destructive'
3299 * $( document.body ).append( button1.$element, button2.$element );
3301 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an
3302 * action, use these flags: **primary** and **safe**.
3303 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3305 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3311 * @param {Object} [config] Configuration options
3312 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary')
3314 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3315 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3316 * @cfg {jQuery} [$flagged] The flagged element. By default,
3317 * the flagged functionality is applied to the element created by the class ($element).
3318 * If a different element is specified, the flagged functionality will be applied to it instead.
3320 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3321 // Configuration initialization
3322 config
= config
|| {};
3326 this.$flagged
= null;
3329 this.setFlags( config
.flags
|| this.constructor.static.flags
);
3330 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3335 OO
.initClass( OO
.ui
.mixin
.FlaggedElement
);
3341 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3342 * parameter contains the name of each modified flag and indicates whether it was
3345 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3346 * that the flag was added, `false` that the flag was removed.
3349 /* Static Properties */
3352 * Initial value to pass to setFlags if no value is provided in config.
3356 * @property {string|string[]|Object.<string, boolean>}
3358 OO
.ui
.mixin
.FlaggedElement
.static.flags
= null;
3363 * Set the flagged element.
3365 * This method is used to retarget a flagged mixin so that its functionality applies to the
3366 * specified element.
3367 * If an element is already set, the method will remove the mixin’s effect on that element.
3369 * @param {jQuery} $flagged Element that should be flagged
3371 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3372 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3373 return 'oo-ui-flaggedElement-' + flag
;
3376 if ( this.$flagged
) {
3377 this.$flagged
.removeClass( classNames
);
3380 this.$flagged
= $flagged
.addClass( classNames
);
3384 * Check if the specified flag is set.
3386 * @param {string} flag Name of flag
3387 * @return {boolean} The flag is set
3389 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3390 // This may be called before the constructor, thus before this.flags is set
3391 return this.flags
&& ( flag
in this.flags
);
3395 * Get the names of all flags set.
3397 * @return {string[]} Flag names
3399 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3400 // This may be called before the constructor, thus before this.flags is set
3401 return Object
.keys( this.flags
|| {} );
3408 * @return {OO.ui.Element} The element, for chaining
3411 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3412 var flag
, className
,
3415 classPrefix
= 'oo-ui-flaggedElement-';
3417 for ( flag
in this.flags
) {
3418 className
= classPrefix
+ flag
;
3419 changes
[ flag
] = false;
3420 delete this.flags
[ flag
];
3421 remove
.push( className
);
3424 if ( this.$flagged
) {
3425 this.$flagged
.removeClass( remove
);
3428 this.updateThemeClasses();
3429 this.emit( 'flag', changes
);
3435 * Add one or more flags.
3437 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3438 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3439 * be added (`true`) or removed (`false`).
3441 * @return {OO.ui.Element} The element, for chaining
3444 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3445 var i
, len
, flag
, className
,
3449 classPrefix
= 'oo-ui-flaggedElement-';
3451 if ( typeof flags
=== 'string' ) {
3452 className
= classPrefix
+ flags
;
3454 if ( !this.flags
[ flags
] ) {
3455 this.flags
[ flags
] = true;
3456 add
.push( className
);
3458 } else if ( Array
.isArray( flags
) ) {
3459 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3461 className
= classPrefix
+ flag
;
3463 if ( !this.flags
[ flag
] ) {
3464 changes
[ flag
] = true;
3465 this.flags
[ flag
] = true;
3466 add
.push( className
);
3469 } else if ( OO
.isPlainObject( flags
) ) {
3470 for ( flag
in flags
) {
3471 className
= classPrefix
+ flag
;
3472 if ( flags
[ flag
] ) {
3474 if ( !this.flags
[ flag
] ) {
3475 changes
[ flag
] = true;
3476 this.flags
[ flag
] = true;
3477 add
.push( className
);
3481 if ( this.flags
[ flag
] ) {
3482 changes
[ flag
] = false;
3483 delete this.flags
[ flag
];
3484 remove
.push( className
);
3490 if ( this.$flagged
) {
3493 .removeClass( remove
);
3496 this.updateThemeClasses();
3497 this.emit( 'flag', changes
);
3503 * TitledElement is mixed into other classes to provide a `title` attribute.
3504 * Titles are rendered by the browser and are made visible when the user moves
3505 * the mouse over the element. Titles are not visible on touch devices.
3508 * // TitledElement provides a `title` attribute to the
3509 * // ButtonWidget class.
3510 * var button = new OO.ui.ButtonWidget( {
3511 * label: 'Button with Title',
3512 * title: 'I am a button'
3514 * $( document.body ).append( button.$element );
3520 * @param {Object} [config] Configuration options
3521 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3522 * If this config is omitted, the title functionality is applied to $element, the
3523 * element created by the class.
3524 * @cfg {string|Function} [title] The title text or a function that returns text. If
3525 * this config is omitted, the value of the {@link #static-title static title} property is used.
3527 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3528 // Configuration initialization
3529 config
= config
|| {};
3532 this.$titled
= null;
3536 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3537 this.setTitledElement( config
.$titled
|| this.$element
);
3542 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3544 /* Static Properties */
3547 * The title text, a function that returns text, or `null` for no title. The value of the static
3548 * property is overridden if the #title config option is used.
3550 * If the element has a default title (e.g. `<input type=file>`), `null` will allow that title to be
3551 * shown. Use empty string to suppress it.
3555 * @property {string|Function|null}
3557 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3562 * Set the titled element.
3564 * This method is used to retarget a TitledElement mixin so that its functionality applies to the
3565 * specified element.
3566 * If an element is already set, the mixin’s effect on that element is removed before the new
3567 * element is set up.
3569 * @param {jQuery} $titled Element that should use the 'titled' functionality
3571 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3572 if ( this.$titled
) {
3573 this.$titled
.removeAttr( 'title' );
3576 this.$titled
= $titled
;
3583 * @param {string|Function|null} title Title text, a function that returns text, or `null`
3586 * @return {OO.ui.Element} The element, for chaining
3588 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3589 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3590 title
= typeof title
=== 'string' ? title
: null;
3592 if ( this.title
!== title
) {
3601 * Update the title attribute, in case of changes to title or accessKey.
3605 * @return {OO.ui.Element} The element, for chaining
3607 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3608 var title
= this.getTitle();
3609 if ( this.$titled
) {
3610 if ( title
!== null ) {
3611 // Only if this is an AccessKeyedElement
3612 if ( this.formatTitleWithAccessKey
) {
3613 title
= this.formatTitleWithAccessKey( title
);
3615 this.$titled
.attr( 'title', title
);
3617 this.$titled
.removeAttr( 'title' );
3626 * @return {string} Title string
3628 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3633 * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
3634 * Access keys allow an user to go to a specific element by using
3635 * a shortcut combination of a browser specific keys + the key
3639 * // AccessKeyedElement provides an `accesskey` attribute to the
3640 * // ButtonWidget class.
3641 * var button = new OO.ui.ButtonWidget( {
3642 * label: 'Button with access key',
3645 * $( document.body ).append( button.$element );
3651 * @param {Object} [config] Configuration options
3652 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3653 * If this config is omitted, the access key functionality is applied to $element, the
3654 * element created by the class.
3655 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3656 * this config is omitted, no access key will be added.
3658 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3659 // Configuration initialization
3660 config
= config
|| {};
3663 this.$accessKeyed
= null;
3664 this.accessKey
= null;
3667 this.setAccessKey( config
.accessKey
|| null );
3668 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3670 // If this is also a TitledElement and it initialized before we did, we may have
3671 // to update the title with the access key
3672 if ( this.updateTitle
) {
3679 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3681 /* Static Properties */
3684 * The access key, a function that returns a key, or `null` for no access key.
3688 * @property {string|Function|null}
3690 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3695 * Set the access keyed element.
3697 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to
3698 * the specified element.
3699 * If an element is already set, the mixin's effect on that element is removed before the new
3700 * element is set up.
3702 * @param {jQuery} $accessKeyed Element that should use the 'access keyed' functionality
3704 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3705 if ( this.$accessKeyed
) {
3706 this.$accessKeyed
.removeAttr( 'accesskey' );
3709 this.$accessKeyed
= $accessKeyed
;
3710 if ( this.accessKey
) {
3711 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3718 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no
3721 * @return {OO.ui.Element} The element, for chaining
3723 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3724 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3726 if ( this.accessKey
!== accessKey
) {
3727 if ( this.$accessKeyed
) {
3728 if ( accessKey
!== null ) {
3729 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3731 this.$accessKeyed
.removeAttr( 'accesskey' );
3734 this.accessKey
= accessKey
;
3736 // Only if this is a TitledElement
3737 if ( this.updateTitle
) {
3748 * @return {string} accessKey string
3750 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3751 return this.accessKey
;
3755 * Add information about the access key to the element's tooltip label.
3756 * (This is only public for hacky usage in FieldLayout.)
3758 * @param {string} title Tooltip label for `title` attribute
3761 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3764 if ( !this.$accessKeyed
) {
3765 // Not initialized yet; the constructor will call updateTitle() which will rerun this
3769 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the
3771 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3772 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3774 accessKey
= this.getAccessKey();
3777 title
+= ' [' + accessKey
+ ']';
3783 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3784 * feels, and functionality can be customized via the class’s configuration options
3785 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3788 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3791 * // A button widget.
3792 * var button = new OO.ui.ButtonWidget( {
3793 * label: 'Button with Icon',
3797 * $( document.body ).append( button.$element );
3799 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3802 * @extends OO.ui.Widget
3803 * @mixins OO.ui.mixin.ButtonElement
3804 * @mixins OO.ui.mixin.IconElement
3805 * @mixins OO.ui.mixin.IndicatorElement
3806 * @mixins OO.ui.mixin.LabelElement
3807 * @mixins OO.ui.mixin.TitledElement
3808 * @mixins OO.ui.mixin.FlaggedElement
3809 * @mixins OO.ui.mixin.TabIndexedElement
3810 * @mixins OO.ui.mixin.AccessKeyedElement
3813 * @param {Object} [config] Configuration options
3814 * @cfg {boolean} [active=false] Whether button should be shown as active
3815 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3816 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3817 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3819 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3820 // Configuration initialization
3821 config
= config
|| {};
3823 // Parent constructor
3824 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3826 // Mixin constructors
3827 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3828 OO
.ui
.mixin
.IconElement
.call( this, config
);
3829 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3830 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3831 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
3832 $titled
: this.$button
3834 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3835 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
3836 $tabIndexed
: this.$button
3838 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
3839 $accessKeyed
: this.$button
3845 this.noFollow
= false;
3848 this.connect( this, {
3849 disable
: 'onDisable'
3853 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3855 .addClass( 'oo-ui-buttonWidget' )
3856 .append( this.$button
);
3857 this.setActive( config
.active
);
3858 this.setHref( config
.href
);
3859 this.setTarget( config
.target
);
3860 this.setNoFollow( config
.noFollow
);
3865 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3866 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3867 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3868 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3869 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3870 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3871 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3872 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3873 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3875 /* Static Properties */
3881 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3887 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3892 * Get hyperlink location.
3894 * @return {string} Hyperlink location
3896 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3901 * Get hyperlink target.
3903 * @return {string} Hyperlink target
3905 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3910 * Get search engine traversal hint.
3912 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3914 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3915 return this.noFollow
;
3919 * Set hyperlink location.
3921 * @param {string|null} href Hyperlink location, null to remove
3923 * @return {OO.ui.Widget} The widget, for chaining
3925 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3926 href
= typeof href
=== 'string' ? href
: null;
3927 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3931 if ( href
!== this.href
) {
3940 * Update the `href` attribute, in case of changes to href or
3945 * @return {OO.ui.Widget} The widget, for chaining
3947 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3948 if ( this.href
!== null && !this.isDisabled() ) {
3949 this.$button
.attr( 'href', this.href
);
3951 this.$button
.removeAttr( 'href' );
3958 * Handle disable events.
3961 * @param {boolean} disabled Element is disabled
3963 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3968 * Set hyperlink target.
3970 * @param {string|null} target Hyperlink target, null to remove
3971 * @return {OO.ui.Widget} The widget, for chaining
3973 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3974 target
= typeof target
=== 'string' ? target
: null;
3976 if ( target
!== this.target
) {
3977 this.target
= target
;
3978 if ( target
!== null ) {
3979 this.$button
.attr( 'target', target
);
3981 this.$button
.removeAttr( 'target' );
3989 * Set search engine traversal hint.
3991 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3992 * @return {OO.ui.Widget} The widget, for chaining
3994 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3995 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3997 if ( noFollow
!== this.noFollow
) {
3998 this.noFollow
= noFollow
;
4000 this.$button
.attr( 'rel', 'nofollow' );
4002 this.$button
.removeAttr( 'rel' );
4009 // Override method visibility hints from ButtonElement
4020 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
4021 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
4022 * removed, and cleared from the group.
4025 * // A ButtonGroupWidget with two buttons.
4026 * var button1 = new OO.ui.PopupButtonWidget( {
4027 * label: 'Select a category',
4030 * $content: $( '<p>List of categories…</p>' ),
4035 * button2 = new OO.ui.ButtonWidget( {
4038 * buttonGroup = new OO.ui.ButtonGroupWidget( {
4039 * items: [ button1, button2 ]
4041 * $( document.body ).append( buttonGroup.$element );
4044 * @extends OO.ui.Widget
4045 * @mixins OO.ui.mixin.GroupElement
4046 * @mixins OO.ui.mixin.TitledElement
4049 * @param {Object} [config] Configuration options
4050 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
4052 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
4053 // Configuration initialization
4054 config
= config
|| {};
4056 // Parent constructor
4057 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
4059 // Mixin constructors
4060 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {
4061 $group
: this.$element
4063 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4066 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
4067 if ( Array
.isArray( config
.items
) ) {
4068 this.addItems( config
.items
);
4074 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
4075 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
4076 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.TitledElement
);
4078 /* Static Properties */
4084 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
4092 * @return {OO.ui.Widget} The widget, for chaining
4094 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
4095 if ( !this.isDisabled() ) {
4096 if ( this.items
[ 0 ] ) {
4097 this.items
[ 0 ].focus();
4106 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
4111 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}.
4112 * In general, IconWidgets should be used with OO.ui.LabelWidget, which creates a label that
4113 * identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
4114 * for a list of icons included in the library.
4117 * // An IconWidget with a label via LabelWidget.
4118 * var myIcon = new OO.ui.IconWidget( {
4122 * // Create a label.
4123 * iconLabel = new OO.ui.LabelWidget( {
4126 * $( document.body ).append( myIcon.$element, iconLabel.$element );
4128 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4131 * @extends OO.ui.Widget
4132 * @mixins OO.ui.mixin.IconElement
4133 * @mixins OO.ui.mixin.TitledElement
4134 * @mixins OO.ui.mixin.LabelElement
4135 * @mixins OO.ui.mixin.FlaggedElement
4138 * @param {Object} [config] Configuration options
4140 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
4141 // Configuration initialization
4142 config
= config
|| {};
4144 // Parent constructor
4145 OO
.ui
.IconWidget
.parent
.call( this, config
);
4147 // Mixin constructors
4148 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {
4149 $icon
: this.$element
4151 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4152 $titled
: this.$element
4154 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4155 $label
: this.$element
,
4156 invisibleLabel
: true
4158 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {
4159 $flagged
: this.$element
4163 this.$element
.addClass( 'oo-ui-iconWidget' );
4164 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4165 // nested in other widgets, because this widget used to not mix in LabelElement.
4166 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4171 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
4172 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
4173 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
4174 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.LabelElement
);
4175 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
4177 /* Static Properties */
4183 OO
.ui
.IconWidget
.static.tagName
= 'span';
4186 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4187 * attention to the status of an item or to clarify the function within a control. For a list of
4188 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4191 * // An indicator widget.
4192 * var indicator1 = new OO.ui.IndicatorWidget( {
4193 * indicator: 'required'
4195 * // Create a fieldset layout to add a label.
4196 * fieldset = new OO.ui.FieldsetLayout();
4197 * fieldset.addItems( [
4198 * new OO.ui.FieldLayout( indicator1, {
4199 * label: 'A required indicator:'
4202 * $( document.body ).append( fieldset.$element );
4204 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4207 * @extends OO.ui.Widget
4208 * @mixins OO.ui.mixin.IndicatorElement
4209 * @mixins OO.ui.mixin.TitledElement
4210 * @mixins OO.ui.mixin.LabelElement
4213 * @param {Object} [config] Configuration options
4215 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4216 // Configuration initialization
4217 config
= config
|| {};
4219 // Parent constructor
4220 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4222 // Mixin constructors
4223 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {
4224 $indicator
: this.$element
4226 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4227 $titled
: this.$element
4229 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4230 $label
: this.$element
,
4231 invisibleLabel
: true
4235 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4236 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4237 // nested in other widgets, because this widget used to not mix in LabelElement.
4238 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4243 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4244 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4245 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4246 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.LabelElement
);
4248 /* Static Properties */
4254 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4257 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4258 * be configured with a `label` option that is set to a string, a label node, or a function:
4260 * - String: a plaintext string
4261 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4262 * label that includes a link or special styling, such as a gray color or additional
4263 * graphical elements.
4264 * - Function: a function that will produce a string in the future. Functions are used
4265 * in cases where the value of the label is not currently defined.
4267 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget},
4268 * which will come into focus when the label is clicked.
4271 * // Two LabelWidgets.
4272 * var label1 = new OO.ui.LabelWidget( {
4273 * label: 'plaintext label'
4275 * label2 = new OO.ui.LabelWidget( {
4276 * label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4278 * // Create a fieldset layout with fields for each example.
4279 * fieldset = new OO.ui.FieldsetLayout();
4280 * fieldset.addItems( [
4281 * new OO.ui.FieldLayout( label1 ),
4282 * new OO.ui.FieldLayout( label2 )
4284 * $( document.body ).append( fieldset.$element );
4287 * @extends OO.ui.Widget
4288 * @mixins OO.ui.mixin.LabelElement
4289 * @mixins OO.ui.mixin.TitledElement
4292 * @param {Object} [config] Configuration options
4293 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4294 * Clicking the label will focus the specified input field.
4296 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4297 // Configuration initialization
4298 config
= config
|| {};
4300 // Parent constructor
4301 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4303 // Mixin constructors
4304 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4305 $label
: this.$element
4307 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4310 this.input
= config
.input
;
4314 if ( this.input
.getInputId() ) {
4315 this.$element
.attr( 'for', this.input
.getInputId() );
4317 this.$label
.on( 'click', function () {
4318 this.input
.simulateLabelClick();
4322 this.$element
.addClass( 'oo-ui-labelWidget' );
4327 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4328 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4329 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4331 /* Static Properties */
4337 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4340 * MessageWidget produces a visual component for sending a notice to the user
4341 * with an icon and distinct design noting its purpose. The MessageWidget changes
4342 * its visual presentation based on the type chosen, which also denotes its UX
4346 * @extends OO.ui.Widget
4347 * @mixins OO.ui.mixin.IconElement
4348 * @mixins OO.ui.mixin.LabelElement
4349 * @mixins OO.ui.mixin.TitledElement
4350 * @mixins OO.ui.mixin.FlaggedElement
4353 * @param {Object} [config] Configuration options
4354 * @cfg {string} [type='notice'] The type of the notice widget. This will also
4355 * impact the flags that the widget receives (and hence its CSS design) as well
4356 * as the icon that appears. Available types:
4357 * 'notice', 'error', 'warning', 'success'
4358 * @cfg {boolean} [inline] Set the notice as an inline notice. The default
4359 * is not inline, or 'boxed' style.
4361 OO
.ui
.MessageWidget
= function OoUiMessageWidget( config
) {
4362 // Configuration initialization
4363 config
= config
|| {};
4365 // Parent constructor
4366 OO
.ui
.MessageWidget
.parent
.call( this, config
);
4368 // Mixin constructors
4369 OO
.ui
.mixin
.IconElement
.call( this, config
);
4370 OO
.ui
.mixin
.LabelElement
.call( this, config
);
4371 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4372 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
4375 this.setType( config
.type
);
4376 this.setInline( config
.inline
);
4380 .append( this.$icon
, this.$label
)
4381 .addClass( 'oo-ui-messageWidget' );
4386 OO
.inheritClass( OO
.ui
.MessageWidget
, OO
.ui
.Widget
);
4387 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.IconElement
);
4388 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.LabelElement
);
4389 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.TitledElement
);
4390 OO
.mixinClass( OO
.ui
.MessageWidget
, OO
.ui
.mixin
.FlaggedElement
);
4392 /* Static Properties */
4395 * An object defining the icon name per defined type.
4398 * @property {Object}
4400 OO
.ui
.MessageWidget
.static.iconMap
= {
4401 notice
: 'infoFilled',
4410 * Set the inline state of the widget.
4412 * @param {boolean} inline Widget is inline
4414 OO
.ui
.MessageWidget
.prototype.setInline = function ( inline
) {
4417 if ( this.inline
!== inline
) {
4418 this.inline
= inline
;
4420 .toggleClass( 'oo-ui-messageWidget-block', !this.inline
);
4424 * Set the widget type. The given type must belong to the list of
4425 * legal types set by OO.ui.MessageWidget.static.iconMap
4427 * @param {string} [type] Given type. Defaults to 'notice'
4429 OO
.ui
.MessageWidget
.prototype.setType = function ( type
) {
4431 if ( Object
.keys( this.constructor.static.iconMap
).indexOf( type
) === -1 ) {
4432 type
= 'notice'; // Default
4435 if ( this.type
!== type
) {
4439 this.setFlags( type
);
4441 // Set the icon and its variant
4442 this.setIcon( this.constructor.static.iconMap
[ type
] );
4443 this.$icon
.removeClass( 'oo-ui-image-' + this.type
);
4444 this.$icon
.addClass( 'oo-ui-image-' + type
);
4446 if ( type
=== 'error' ) {
4447 this.$element
.attr( 'role', 'alert' );
4448 this.$element
.removeAttr( 'aria-live' );
4450 this.$element
.removeAttr( 'role' );
4451 this.$element
.attr( 'aria-live', 'polite' );
4459 * PendingElement is a mixin that is used to create elements that notify users that something is
4460 * happening and that they should wait before proceeding. The pending state is visually represented
4461 * with a pending texture that appears in the head of a pending
4462 * {@link OO.ui.ProcessDialog process dialog} or in the input field of a
4463 * {@link OO.ui.TextInputWidget text input widget}.
4465 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked
4466 * as pending, but only when used in {@link OO.ui.MessageDialog message dialogs}. The behavior is
4467 * not currently supported for action widgets used in process dialogs.
4470 * function MessageDialog( config ) {
4471 * MessageDialog.parent.call( this, config );
4473 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4475 * MessageDialog.static.name = 'myMessageDialog';
4476 * MessageDialog.static.actions = [
4477 * { action: 'save', label: 'Done', flags: 'primary' },
4478 * { label: 'Cancel', flags: 'safe' }
4481 * MessageDialog.prototype.initialize = function () {
4482 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4483 * this.content = new OO.ui.PanelLayout( { padded: true } );
4484 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending ' +
4485 * 'state. Note that action widgets can be marked pending in message dialogs but not ' +
4486 * 'process dialogs.</p>' );
4487 * this.$body.append( this.content.$element );
4489 * MessageDialog.prototype.getBodyHeight = function () {
4492 * MessageDialog.prototype.getActionProcess = function ( action ) {
4493 * var dialog = this;
4494 * if ( action === 'save' ) {
4495 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4496 * return new OO.ui.Process()
4498 * .next( function () {
4499 * dialog.getActions().get({actions: 'save'})[0].popPending();
4502 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4505 * var windowManager = new OO.ui.WindowManager();
4506 * $( document.body ).append( windowManager.$element );
4508 * var dialog = new MessageDialog();
4509 * windowManager.addWindows( [ dialog ] );
4510 * windowManager.openWindow( dialog );
4516 * @param {Object} [config] Configuration options
4517 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4519 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4520 // Configuration initialization
4521 config
= config
|| {};
4525 this.$pending
= null;
4528 this.setPendingElement( config
.$pending
|| this.$element
);
4533 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4538 * Set the pending element (and clean up any existing one).
4540 * @param {jQuery} $pending The element to set to pending.
4542 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4543 if ( this.$pending
) {
4544 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4547 this.$pending
= $pending
;
4548 if ( this.pending
> 0 ) {
4549 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4554 * Check if an element is pending.
4556 * @return {boolean} Element is pending
4558 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4559 return !!this.pending
;
4563 * Increase the pending counter. The pending state will remain active until the counter is zero
4564 * (i.e., the number of calls to #pushPending and #popPending is the same).
4567 * @return {OO.ui.Element} The element, for chaining
4569 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4570 if ( this.pending
=== 0 ) {
4571 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4572 this.updateThemeClasses();
4580 * Decrease the pending counter. The pending state will remain active until the counter is zero
4581 * (i.e., the number of calls to #pushPending and #popPending is the same).
4584 * @return {OO.ui.Element} The element, for chaining
4586 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4587 if ( this.pending
=== 1 ) {
4588 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4589 this.updateThemeClasses();
4591 this.pending
= Math
.max( 0, this.pending
- 1 );
4597 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4598 * in the document (for example, in an OO.ui.Window's $overlay).
4600 * The elements's position is automatically calculated and maintained when window is resized or the
4601 * page is scrolled. If you reposition the container manually, you have to call #position to make
4602 * sure the element is still placed correctly.
4604 * As positioning is only possible when both the element and the container are attached to the DOM
4605 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4606 * the #toggle method to display a floating popup, for example.
4612 * @param {Object} [config] Configuration options
4613 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4614 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4615 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4616 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4617 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4618 * 'top': Align the top edge with $floatableContainer's top edge
4619 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4620 * 'center': Vertically align the center with $floatableContainer's center
4621 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4622 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4623 * 'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
4624 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4625 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4626 * 'center': Horizontally align the center with $floatableContainer's center
4627 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4630 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4631 // Configuration initialization
4632 config
= config
|| {};
4635 this.$floatable
= null;
4636 this.$floatableContainer
= null;
4637 this.$floatableWindow
= null;
4638 this.$floatableClosestScrollable
= null;
4639 this.floatableOutOfView
= false;
4640 this.onFloatableScrollHandler
= this.position
.bind( this );
4641 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4644 this.setFloatableContainer( config
.$floatableContainer
);
4645 this.setFloatableElement( config
.$floatable
|| this.$element
);
4646 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4647 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4648 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ?
4649 true : !!config
.hideWhenOutOfView
;
4655 * Set floatable element.
4657 * If an element is already set, it will be cleaned up before setting up the new element.
4659 * @param {jQuery} $floatable Element to make floatable
4661 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4662 if ( this.$floatable
) {
4663 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4664 this.$floatable
.css( { left
: '', top
: '' } );
4667 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4672 * Set floatable container.
4674 * The element will be positioned relative to the specified container.
4676 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4678 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4679 this.$floatableContainer
= $floatableContainer
;
4680 if ( this.$floatable
) {
4686 * Change how the element is positioned vertically.
4688 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4690 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4691 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4692 throw new Error( 'Invalid value for vertical position: ' + position
);
4694 if ( this.verticalPosition
!== position
) {
4695 this.verticalPosition
= position
;
4696 if ( this.$floatable
) {
4703 * Change how the element is positioned horizontally.
4705 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4707 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4708 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4709 throw new Error( 'Invalid value for horizontal position: ' + position
);
4711 if ( this.horizontalPosition
!== position
) {
4712 this.horizontalPosition
= position
;
4713 if ( this.$floatable
) {
4720 * Toggle positioning.
4722 * Do not turn positioning on until after the element is attached to the DOM and visible.
4724 * @param {boolean} [positioning] Enable positioning, omit to toggle
4726 * @return {OO.ui.Element} The element, for chaining
4728 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4729 var closestScrollableOfContainer
;
4731 if ( !this.$floatable
|| !this.$floatableContainer
) {
4735 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4737 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4738 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4739 this.warnedUnattached
= true;
4742 if ( this.positioning
!== positioning
) {
4743 this.positioning
= positioning
;
4745 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer(
4746 this.$floatableContainer
[ 0 ]
4748 // If the scrollable is the root, we have to listen to scroll events
4749 // on the window because of browser inconsistencies.
4750 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4751 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow(
4752 closestScrollableOfContainer
4756 if ( positioning
) {
4757 this.$floatableWindow
= $( this.getElementWindow() );
4758 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4760 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4761 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4763 // Initial position after visible
4766 if ( this.$floatableWindow
) {
4767 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4768 this.$floatableWindow
= null;
4771 if ( this.$floatableClosestScrollable
) {
4772 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4773 this.$floatableClosestScrollable
= null;
4776 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4784 * Check whether the bottom edge of the given element is within the viewport of the given
4788 * @param {jQuery} $element
4789 * @param {jQuery} $container
4792 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4793 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
,
4794 rightEdgeInBounds
, startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4795 direction
= $element
.css( 'direction' );
4797 elemRect
= $element
[ 0 ].getBoundingClientRect();
4798 if ( $container
[ 0 ] === window
) {
4799 viewportSpacing
= OO
.ui
.getViewportSpacing();
4803 right
: document
.documentElement
.clientWidth
,
4804 bottom
: document
.documentElement
.clientHeight
4806 contRect
.top
+= viewportSpacing
.top
;
4807 contRect
.left
+= viewportSpacing
.left
;
4808 contRect
.right
-= viewportSpacing
.right
;
4809 contRect
.bottom
-= viewportSpacing
.bottom
;
4811 contRect
= $container
[ 0 ].getBoundingClientRect();
4814 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4815 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4816 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4817 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4818 if ( direction
=== 'rtl' ) {
4819 startEdgeInBounds
= rightEdgeInBounds
;
4820 endEdgeInBounds
= leftEdgeInBounds
;
4822 startEdgeInBounds
= leftEdgeInBounds
;
4823 endEdgeInBounds
= rightEdgeInBounds
;
4826 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4829 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4832 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4835 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4839 // The other positioning values are all about being inside the container,
4840 // so in those cases all we care about is that any part of the container is visible.
4841 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4842 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4846 * Check if the floatable is hidden to the user because it was offscreen.
4848 * @return {boolean} Floatable is out of view
4850 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4851 return this.floatableOutOfView
;
4855 * Position the floatable below its container.
4857 * This should only be done when both of them are attached to the DOM and visible.
4860 * @return {OO.ui.Element} The element, for chaining
4862 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4863 if ( !this.positioning
) {
4868 // To continue, some things need to be true:
4869 // The element must actually be in the DOM
4870 this.isElementAttached() && (
4871 // The closest scrollable is the current window
4872 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4873 // OR is an element in the element's DOM
4874 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4877 // Abort early if important parts of the widget are no longer attached to the DOM
4881 this.floatableOutOfView
= this.hideWhenOutOfView
&&
4882 !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4883 if ( this.floatableOutOfView
) {
4884 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4887 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4890 this.$floatable
.css( this.computePosition() );
4892 // We updated the position, so re-evaluate the clipping state.
4893 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4894 // will not notice the need to update itself.)
4895 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here.
4896 // Why does it not listen to the right events in the right places?
4905 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4906 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4907 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4909 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4911 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4912 var isBody
, scrollableX
, scrollableY
, containerPos
,
4913 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4914 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4915 direction
= this.$floatableContainer
.css( 'direction' ),
4916 $offsetParent
= this.$floatable
.offsetParent();
4918 if ( $offsetParent
.is( 'html' ) ) {
4919 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4920 // <html> element, but they do work on the <body>
4921 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4923 isBody
= $offsetParent
.is( 'body' );
4924 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' ||
4925 $offsetParent
.css( 'overflow-x' ) === 'auto';
4926 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' ||
4927 $offsetParent
.css( 'overflow-y' ) === 'auto';
4929 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4930 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4931 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container
4932 // is the body, or if it isn't scrollable
4933 scrollTop
= scrollableY
&& !isBody
?
4934 $offsetParent
.scrollTop() : 0;
4935 scrollLeft
= scrollableX
&& !isBody
?
4936 OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4938 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4939 // if the <body> has a margin
4940 containerPos
= isBody
?
4941 this.$floatableContainer
.offset() :
4942 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4943 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4944 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4945 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4946 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4948 if ( this.verticalPosition
=== 'below' ) {
4949 newPos
.top
= containerPos
.bottom
;
4950 } else if ( this.verticalPosition
=== 'above' ) {
4951 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4952 } else if ( this.verticalPosition
=== 'top' ) {
4953 newPos
.top
= containerPos
.top
;
4954 } else if ( this.verticalPosition
=== 'bottom' ) {
4955 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4956 } else if ( this.verticalPosition
=== 'center' ) {
4957 newPos
.top
= containerPos
.top
+
4958 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4961 if ( this.horizontalPosition
=== 'before' ) {
4962 newPos
.end
= containerPos
.start
;
4963 } else if ( this.horizontalPosition
=== 'after' ) {
4964 newPos
.start
= containerPos
.end
;
4965 } else if ( this.horizontalPosition
=== 'start' ) {
4966 newPos
.start
= containerPos
.start
;
4967 } else if ( this.horizontalPosition
=== 'end' ) {
4968 newPos
.end
= containerPos
.end
;
4969 } else if ( this.horizontalPosition
=== 'center' ) {
4970 newPos
.left
= containerPos
.left
+
4971 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4974 if ( newPos
.start
!== undefined ) {
4975 if ( direction
=== 'rtl' ) {
4976 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4977 $offsetParent
).outerWidth() - newPos
.start
;
4979 newPos
.left
= newPos
.start
;
4981 delete newPos
.start
;
4983 if ( newPos
.end
!== undefined ) {
4984 if ( direction
=== 'rtl' ) {
4985 newPos
.left
= newPos
.end
;
4987 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4988 $offsetParent
).outerWidth() - newPos
.end
;
4993 // Account for scroll position
4994 if ( newPos
.top
!== '' ) {
4995 newPos
.top
+= scrollTop
;
4997 if ( newPos
.bottom
!== '' ) {
4998 newPos
.bottom
-= scrollTop
;
5000 if ( newPos
.left
!== '' ) {
5001 newPos
.left
+= scrollLeft
;
5003 if ( newPos
.right
!== '' ) {
5004 newPos
.right
-= scrollLeft
;
5007 // Account for scrollbar gutter
5008 if ( newPos
.bottom
!== '' ) {
5009 newPos
.bottom
-= horizScrollbarHeight
;
5011 if ( direction
=== 'rtl' ) {
5012 if ( newPos
.left
!== '' ) {
5013 newPos
.left
-= vertScrollbarWidth
;
5016 if ( newPos
.right
!== '' ) {
5017 newPos
.right
-= vertScrollbarWidth
;
5025 * Element that can be automatically clipped to visible boundaries.
5027 * Whenever the element's natural height changes, you have to call
5028 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
5029 * clipping correctly.
5031 * The dimensions of #$clippableContainer will be compared to the boundaries of the
5032 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
5033 * then #$clippable will be given a fixed reduced height and/or width and will be made
5034 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
5035 * but you can build a static footer by setting #$clippableContainer to an element that contains
5036 * #$clippable and the footer.
5042 * @param {Object} [config] Configuration options
5043 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
5044 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
5045 * omit to use #$clippable
5047 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
5048 // Configuration initialization
5049 config
= config
|| {};
5052 this.$clippable
= null;
5053 this.$clippableContainer
= null;
5054 this.clipping
= false;
5055 this.clippedHorizontally
= false;
5056 this.clippedVertically
= false;
5057 this.$clippableScrollableContainer
= null;
5058 this.$clippableScroller
= null;
5059 this.$clippableWindow
= null;
5060 this.idealWidth
= null;
5061 this.idealHeight
= null;
5062 this.onClippableScrollHandler
= this.clip
.bind( this );
5063 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
5066 if ( config
.$clippableContainer
) {
5067 this.setClippableContainer( config
.$clippableContainer
);
5069 this.setClippableElement( config
.$clippable
|| this.$element
);
5075 * Set clippable element.
5077 * If an element is already set, it will be cleaned up before setting up the new element.
5079 * @param {jQuery} $clippable Element to make clippable
5081 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
5082 if ( this.$clippable
) {
5083 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
5084 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
5085 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5088 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
5093 * Set clippable container.
5095 * This is the container that will be measured when deciding whether to clip. When clipping,
5096 * #$clippable will be resized in order to keep the clippable container fully visible.
5098 * If the clippable container is unset, #$clippable will be used.
5100 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
5102 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
5103 this.$clippableContainer
= $clippableContainer
;
5104 if ( this.$clippable
) {
5112 * Do not turn clipping on until after the element is attached to the DOM and visible.
5114 * @param {boolean} [clipping] Enable clipping, omit to toggle
5116 * @return {OO.ui.Element} The element, for chaining
5118 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
5119 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
5121 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5122 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
5123 this.warnedUnattached
= true;
5126 if ( this.clipping
!== clipping
) {
5127 this.clipping
= clipping
;
5129 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
5130 // If the clippable container is the root, we have to listen to scroll events and check
5131 // jQuery.scrollTop on the window because of browser inconsistencies
5132 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
5133 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
5134 this.$clippableScrollableContainer
;
5135 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
5136 this.$clippableWindow
= $( this.getElementWindow() )
5137 .on( 'resize', this.onClippableWindowResizeHandler
);
5138 // Initial clip after visible
5141 this.$clippable
.css( {
5149 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5151 this.$clippableScrollableContainer
= null;
5152 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
5153 this.$clippableScroller
= null;
5154 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
5155 this.$clippableWindow
= null;
5163 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5165 * @return {boolean} Element will be clipped to the visible area
5167 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
5168 return this.clipping
;
5172 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5174 * @return {boolean} Part of the element is being clipped
5176 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
5177 return this.clippedHorizontally
|| this.clippedVertically
;
5181 * Check if the right of the element is being clipped by the nearest scrollable container.
5183 * @return {boolean} Part of the element is being clipped
5185 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
5186 return this.clippedHorizontally
;
5190 * Check if the bottom of the element is being clipped by the nearest scrollable container.
5192 * @return {boolean} Part of the element is being clipped
5194 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
5195 return this.clippedVertically
;
5199 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
5201 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
5202 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
5204 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
5205 this.idealWidth
= width
;
5206 this.idealHeight
= height
;
5208 if ( !this.clipping
) {
5209 // Update dimensions
5210 this.$clippable
.css( { width
: width
, height
: height
} );
5212 // While clipping, idealWidth and idealHeight are not considered
5216 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5217 * ClippableElement will clip the opposite side when reducing element's width.
5219 * Classes that mix in ClippableElement should override this to return 'right' if their
5220 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
5221 * If your class also mixes in FloatableElement, this is handled automatically.
5223 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5224 * always in pixels, even if they were unset or set to 'auto'.)
5226 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
5228 * @return {string} 'left' or 'right'
5230 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
5231 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
5238 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5239 * ClippableElement will clip the opposite side when reducing element's width.
5241 * Classes that mix in ClippableElement should override this to return 'bottom' if their
5242 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
5243 * If your class also mixes in FloatableElement, this is handled automatically.
5245 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5246 * always in pixels, even if they were unset or set to 'auto'.)
5248 * When in doubt, 'top' is a sane fallback.
5250 * @return {string} 'top' or 'bottom'
5252 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
5253 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
5260 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5261 * when the element's natural height changes.
5263 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5264 * overlapped by, the visible area of the nearest scrollable container.
5266 * Because calling clip() when the natural height changes isn't always possible, we also set
5267 * max-height when the element isn't being clipped. This means that if the element tries to grow
5268 * beyond the edge, something reasonable will happen before clip() is called.
5271 * @return {OO.ui.Element} The element, for chaining
5273 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
5274 var extraHeight
, extraWidth
, viewportSpacing
,
5275 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
5276 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
5277 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
5278 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
5279 // Extra tolerance so that the sloppy code below doesn't result in results that are off
5280 // by one or two pixels. (And also so that we have space to display drop shadows.)
5281 // Chosen by fair dice roll.
5284 if ( !this.clipping
) {
5285 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below
5290 function rectIntersection( a
, b
) {
5292 out
.top
= Math
.max( a
.top
, b
.top
);
5293 out
.left
= Math
.max( a
.left
, b
.left
);
5294 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
5295 out
.right
= Math
.min( a
.right
, b
.right
);
5299 viewportSpacing
= OO
.ui
.getViewportSpacing();
5301 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
5302 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
5303 // Dimensions of the browser window, rather than the element!
5307 right
: document
.documentElement
.clientWidth
,
5308 bottom
: document
.documentElement
.clientHeight
5310 viewportRect
.top
+= viewportSpacing
.top
;
5311 viewportRect
.left
+= viewportSpacing
.left
;
5312 viewportRect
.right
-= viewportSpacing
.right
;
5313 viewportRect
.bottom
-= viewportSpacing
.bottom
;
5315 $viewport
= this.$clippableScrollableContainer
;
5316 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
5317 // Convert into a plain object
5318 viewportRect
= $.extend( {}, viewportRect
);
5321 // Account for scrollbar gutter
5322 direction
= $viewport
.css( 'direction' );
5323 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
5324 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
5325 viewportRect
.bottom
-= horizScrollbarHeight
;
5326 if ( direction
=== 'rtl' ) {
5327 viewportRect
.left
+= vertScrollbarWidth
;
5329 viewportRect
.right
-= vertScrollbarWidth
;
5332 // Add arbitrary tolerance
5333 viewportRect
.top
+= buffer
;
5334 viewportRect
.left
+= buffer
;
5335 viewportRect
.right
-= buffer
;
5336 viewportRect
.bottom
-= buffer
;
5338 $item
= this.$clippableContainer
|| this.$clippable
;
5340 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
5341 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
5343 itemRect
= $item
[ 0 ].getBoundingClientRect();
5344 // Convert into a plain object
5345 itemRect
= $.extend( {}, itemRect
);
5347 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5348 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5349 if ( this.getHorizontalAnchorEdge() === 'right' ) {
5350 itemRect
.left
= viewportRect
.left
;
5352 itemRect
.right
= viewportRect
.right
;
5354 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5355 itemRect
.top
= viewportRect
.top
;
5357 itemRect
.bottom
= viewportRect
.bottom
;
5360 availableRect
= rectIntersection( viewportRect
, itemRect
);
5362 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5363 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5364 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5365 desiredWidth
= Math
.min( desiredWidth
,
5366 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5367 desiredHeight
= Math
.min( desiredHeight
,
5368 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5369 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5370 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5371 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5372 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5373 clipWidth
= allotedWidth
< naturalWidth
;
5374 clipHeight
= allotedHeight
< naturalHeight
;
5377 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5379 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5381 this.$clippable
.css( 'overflowX', 'scroll' );
5382 // eslint-disable-next-line no-void
5383 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5384 this.$clippable
.css( {
5385 width
: Math
.max( 0, allotedWidth
),
5389 this.$clippable
.css( {
5391 width
: this.idealWidth
|| '',
5392 maxWidth
: Math
.max( 0, allotedWidth
)
5396 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5398 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5400 this.$clippable
.css( 'overflowY', 'scroll' );
5401 // eslint-disable-next-line no-void
5402 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5403 this.$clippable
.css( {
5404 height
: Math
.max( 0, allotedHeight
),
5408 this.$clippable
.css( {
5410 height
: this.idealHeight
|| '',
5411 maxHeight
: Math
.max( 0, allotedHeight
)
5415 // If we stopped clipping in at least one of the dimensions
5416 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5417 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5420 this.clippedHorizontally
= clipWidth
;
5421 this.clippedVertically
= clipHeight
;
5427 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5428 * By default, each popup has an anchor that points toward its origin.
5429 * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
5431 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5435 * var popup = new OO.ui.PopupWidget( {
5436 * $content: $( '<p>Hi there!</p>' ),
5441 * $( document.body ).append( popup.$element );
5442 * // To display the popup, toggle the visibility to 'true'.
5443 * popup.toggle( true );
5445 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5448 * @extends OO.ui.Widget
5449 * @mixins OO.ui.mixin.LabelElement
5450 * @mixins OO.ui.mixin.ClippableElement
5451 * @mixins OO.ui.mixin.FloatableElement
5454 * @param {Object} [config] Configuration options
5455 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5456 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5457 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5458 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5459 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5460 * of $floatableContainer
5461 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5462 * of $floatableContainer
5463 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5464 * endwards (right/left) to the vertical center of $floatableContainer
5465 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5466 * startwards (left/right) to the vertical center of $floatableContainer
5467 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5468 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in
5469 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5470 * move the popup as far downwards as possible.
5471 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in
5472 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5473 * move the popup as far upwards as possible.
5474 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the
5475 * center of the popup with the center of $floatableContainer.
5476 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5477 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5478 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5479 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5480 * desired direction to display the popup without clipping
5481 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5482 * See the [OOUI docs on MediaWiki][3] for an example.
5483 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5484 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a
5486 * @cfg {jQuery} [$content] Content to append to the popup's body
5487 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5488 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5489 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5490 * This config option is only relevant if #autoClose is set to `true`. See the
5491 * [OOUI documentation on MediaWiki][2] for an example.
5492 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5493 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5495 * @cfg {boolean} [padded=false] Add padding to the popup's body
5497 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5498 // Configuration initialization
5499 config
= config
|| {};
5501 // Parent constructor
5502 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5504 // Properties (must be set before ClippableElement constructor call)
5505 this.$body
= $( '<div>' );
5506 this.$popup
= $( '<div>' );
5508 // Mixin constructors
5509 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5510 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {
5511 $clippable
: this.$body
,
5512 $clippableContainer
: this.$popup
5514 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5517 this.$anchor
= $( '<div>' );
5518 // If undefined, will be computed lazily in computePosition()
5519 this.$container
= config
.$container
;
5520 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5521 this.autoClose
= !!config
.autoClose
;
5522 this.transitionTimeout
= null;
5523 this.anchored
= false;
5524 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5525 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5528 this.setSize( config
.width
, config
.height
);
5529 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5530 this.setAlignment( config
.align
|| 'center' );
5531 this.setPosition( config
.position
|| 'below' );
5532 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5533 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5534 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5535 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5537 .addClass( 'oo-ui-popupWidget-popup' )
5538 .append( this.$body
);
5540 .addClass( 'oo-ui-popupWidget' )
5541 .append( this.$popup
, this.$anchor
);
5542 // Move content, which was added to #$element by OO.ui.Widget, to the body
5543 // FIXME This is gross, we should use '$body' or something for the config
5544 if ( config
.$content
instanceof $ ) {
5545 this.$body
.append( config
.$content
);
5548 if ( config
.padded
) {
5549 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5552 if ( config
.head
) {
5553 this.closeButton
= new OO
.ui
.ButtonWidget( {
5557 this.closeButton
.connect( this, {
5558 click
: 'onCloseButtonClick'
5560 this.$head
= $( '<div>' )
5561 .addClass( 'oo-ui-popupWidget-head' )
5562 .append( this.$label
, this.closeButton
.$element
);
5563 this.$popup
.prepend( this.$head
);
5566 if ( config
.$footer
) {
5567 this.$footer
= $( '<div>' )
5568 .addClass( 'oo-ui-popupWidget-footer' )
5569 .append( config
.$footer
);
5570 this.$popup
.append( this.$footer
);
5573 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5574 // that reference properties not initialized at that time of parent class construction
5575 // TODO: Find a better way to handle post-constructor setup
5576 this.visible
= false;
5577 this.$element
.addClass( 'oo-ui-element-hidden' );
5582 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5583 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5584 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5585 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5592 * The popup is ready: it is visible and has been positioned and clipped.
5598 * Handles document mouse down events.
5601 * @param {MouseEvent} e Mouse down event
5603 OO
.ui
.PopupWidget
.prototype.onDocumentMouseDown = function ( e
) {
5606 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5608 this.toggle( false );
5613 * Bind document mouse down listener.
5617 OO
.ui
.PopupWidget
.prototype.bindDocumentMouseDownListener = function () {
5618 // Capture clicks outside popup
5619 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5620 // We add 'click' event because iOS safari needs to respond to this event.
5621 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5622 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5623 // of occasionally not emitting 'click' properly, that event seems to be the standard
5624 // that it should be emitting, so we add it to this and will operate the event handler
5625 // on whichever of these events was triggered first
5626 this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5630 * Handles close button click events.
5634 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5635 if ( this.isVisible() ) {
5636 this.toggle( false );
5641 * Unbind document mouse down listener.
5645 OO
.ui
.PopupWidget
.prototype.unbindDocumentMouseDownListener = function () {
5646 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5647 this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5651 * Handles document key down events.
5654 * @param {KeyboardEvent} e Key down event
5656 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5658 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5661 this.toggle( false );
5663 e
.stopPropagation();
5668 * Bind document key down listener.
5672 OO
.ui
.PopupWidget
.prototype.bindDocumentKeyDownListener = function () {
5673 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5677 * Unbind document key down listener.
5681 OO
.ui
.PopupWidget
.prototype.unbindDocumentKeyDownListener = function () {
5682 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5686 * Show, hide, or toggle the visibility of the anchor.
5688 * @param {boolean} [show] Show anchor, omit to toggle
5690 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5691 show
= show
=== undefined ? !this.anchored
: !!show
;
5693 if ( this.anchored
!== show
) {
5695 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5696 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5698 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5699 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5701 this.anchored
= show
;
5706 * Change which edge the anchor appears on.
5708 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5710 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5711 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5712 throw new Error( 'Invalid value for edge: ' + edge
);
5714 if ( this.anchorEdge
!== null ) {
5715 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5717 this.anchorEdge
= edge
;
5718 if ( this.anchored
) {
5719 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5724 * Check if the anchor is visible.
5726 * @return {boolean} Anchor is visible
5728 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5729 return this.anchored
;
5733 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5734 * `.toggle( true )` after its #$element is attached to the DOM.
5736 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5737 * it in the right place and with the right dimensions only work correctly while it is attached.
5738 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5739 * strictly enforced, so currently it only generates a warning in the browser console.
5744 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5745 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5746 show
= show
=== undefined ? !this.isVisible() : !!show
;
5748 change
= show
!== this.isVisible();
5750 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5751 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5752 this.warnedUnattached
= true;
5754 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5755 // Fall back to the parent node if the floatableContainer is not set
5756 this.setFloatableContainer( this.$element
.parent() );
5759 if ( change
&& show
&& this.autoFlip
) {
5760 // Reset auto-flipping before showing the popup again. It's possible we no longer need to
5761 // flip (e.g. if the user scrolled).
5762 this.isAutoFlipped
= false;
5766 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5769 this.togglePositioning( show
&& !!this.$floatableContainer
);
5772 if ( this.autoClose
) {
5773 this.bindDocumentMouseDownListener();
5774 this.bindDocumentKeyDownListener();
5776 this.updateDimensions();
5777 this.toggleClipping( true );
5779 if ( this.autoFlip
) {
5780 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5781 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5782 // If opening the popup in the normal direction causes it to be clipped,
5783 // open in the opposite one instead
5784 normalHeight
= this.$element
.height();
5785 this.isAutoFlipped
= !this.isAutoFlipped
;
5787 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5788 // If that also causes it to be clipped, open in whichever direction
5789 // we have more space
5790 oppositeHeight
= this.$element
.height();
5791 if ( oppositeHeight
< normalHeight
) {
5792 this.isAutoFlipped
= !this.isAutoFlipped
;
5798 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5799 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5800 // If opening the popup in the normal direction causes it to be clipped,
5801 // open in the opposite one instead
5802 normalWidth
= this.$element
.width();
5803 this.isAutoFlipped
= !this.isAutoFlipped
;
5804 // Due to T180173 horizontally clipped PopupWidgets have messed up
5805 // dimensions, which causes positioning to be off. Toggle clipping back and
5806 // forth to work around.
5807 this.toggleClipping( false );
5809 this.toggleClipping( true );
5810 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5811 // If that also causes it to be clipped, open in whichever direction
5812 // we have more space
5813 oppositeWidth
= this.$element
.width();
5814 if ( oppositeWidth
< normalWidth
) {
5815 this.isAutoFlipped
= !this.isAutoFlipped
;
5816 // Due to T180173, horizontally clipped PopupWidgets have messed up
5817 // dimensions, which causes positioning to be off. Toggle clipping
5818 // back and forth to work around.
5819 this.toggleClipping( false );
5821 this.toggleClipping( true );
5828 this.emit( 'ready' );
5830 this.toggleClipping( false );
5831 if ( this.autoClose
) {
5832 this.unbindDocumentMouseDownListener();
5833 this.unbindDocumentKeyDownListener();
5842 * Set the size of the popup.
5844 * Changing the size may also change the popup's position depending on the alignment.
5846 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5847 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5848 * @param {boolean} [transition=false] Use a smooth transition
5851 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5852 this.width
= width
!== undefined ? width
: 320;
5853 this.height
= height
!== undefined ? height
: null;
5854 if ( this.isVisible() ) {
5855 this.updateDimensions( transition
);
5860 * Update the size and position.
5862 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5863 * be called automatically.
5865 * @param {boolean} [transition=false] Use a smooth transition
5868 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5871 // Prevent transition from being interrupted
5872 clearTimeout( this.transitionTimeout
);
5874 // Enable transition
5875 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5881 // Prevent transitioning after transition is complete
5882 this.transitionTimeout
= setTimeout( function () {
5883 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5886 // Prevent transitioning immediately
5887 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5894 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5895 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
,
5896 anchorPos
, anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
,
5897 floatablePos
, offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5899 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5900 popupPositionOppositeMap
= {
5908 'force-left': 'backwards',
5909 'force-right': 'forwards'
5912 'force-left': 'forwards',
5913 'force-right': 'backwards'
5925 backwards
: this.anchored
? 'before' : 'end'
5933 if ( !this.$container
) {
5934 // Lazy-initialize $container if not specified in constructor
5935 this.$container
= $( this.getClosestScrollableElementContainer() );
5937 direction
= this.$container
.css( 'direction' );
5939 // Set height and width before we do anything else, since it might cause our measurements
5940 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5942 width
: this.width
!== null ? this.width
: 'auto',
5943 height
: this.height
!== null ? this.height
: 'auto'
5946 align
= alignMap
[ direction
][ this.align
] || this.align
;
5947 popupPosition
= this.popupPosition
;
5948 if ( this.isAutoFlipped
) {
5949 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5952 // If the popup is positioned before or after, then the anchor positioning is vertical,
5953 // otherwise horizontal
5954 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5955 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5956 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5957 near
= vertical
? 'top' : 'left';
5958 far
= vertical
? 'bottom' : 'right';
5959 sizeProp
= vertical
? 'Height' : 'Width';
5960 popupSize
= vertical
?
5961 ( this.height
|| this.$popup
.height() ) :
5962 ( this.width
|| this.$popup
.width() );
5964 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5965 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5966 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5969 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5970 // Find out which property FloatableElement used for positioning, and adjust that value
5971 positionProp
= vertical
?
5972 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5973 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5975 // Figure out where the near and far edges of the popup and $floatableContainer are
5976 floatablePos
= this.$floatableContainer
.offset();
5977 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5978 // Measure where the offsetParent is and compute our position based on that and parentPosition
5979 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5980 { top
: 0, left
: 0 } :
5981 this.$element
.offsetParent().offset();
5983 if ( positionProp
=== near
) {
5984 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5985 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5987 popupPos
[ far
] = offsetParentPos
[ near
] +
5988 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5989 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5992 if ( this.anchored
) {
5993 // Position the anchor (which is positioned relative to the popup) to point to
5994 // $floatableContainer
5995 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5996 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5998 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more
5999 // space this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use
6000 // scrollWidth/Height
6001 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
6002 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
6003 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
6004 // Not enough space for the anchor on the start side; pull the popup startwards
6005 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
6006 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
6007 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
6008 // Not enough space for the anchor on the end side; pull the popup endwards
6009 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
6010 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
6012 positionAdjustment
= 0;
6015 positionAdjustment
= 0;
6018 // Check if the popup will go beyond the edge of this.$container
6019 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
6020 { top
: 0, left
: 0 } :
6021 this.$container
.offset();
6022 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
6023 if ( this.$container
[ 0 ] === document
.documentElement
) {
6024 viewportSpacing
= OO
.ui
.getViewportSpacing();
6025 containerPos
[ near
] += viewportSpacing
[ near
];
6026 containerPos
[ far
] -= viewportSpacing
[ far
];
6028 // Take into account how much the popup will move because of the adjustments we're going to make
6029 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
6030 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
6031 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
6032 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
6033 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
6034 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
6035 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
6036 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
6037 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
6038 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
6041 if ( this.anchored
) {
6042 // Adjust anchorOffset for positionAdjustment
6043 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
6045 // Position the anchor
6046 anchorCss
[ start
] = anchorOffset
;
6047 this.$anchor
.css( anchorCss
);
6050 // Move the popup if needed
6051 parentPosition
[ positionProp
] += positionAdjustment
;
6053 return parentPosition
;
6057 * Set popup alignment
6059 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
6060 * `backwards` or `forwards`.
6062 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
6063 // Validate alignment
6064 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
6067 this.align
= 'center';
6073 * Get popup alignment
6075 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
6076 * `backwards` or `forwards`.
6078 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
6083 * Change the positioning of the popup.
6085 * @param {string} position 'above', 'below', 'before' or 'after'
6087 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
6088 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
6091 this.popupPosition
= position
;
6096 * Get popup positioning.
6098 * @return {string} 'above', 'below', 'before' or 'after'
6100 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
6101 return this.popupPosition
;
6105 * Set popup auto-flipping.
6107 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
6108 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
6109 * desired direction to display the popup without clipping
6111 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
6112 autoFlip
= !!autoFlip
;
6114 if ( this.autoFlip
!== autoFlip
) {
6115 this.autoFlip
= autoFlip
;
6120 * Set which elements will not close the popup when clicked.
6122 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
6124 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
6126 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
6127 this.$autoCloseIgnore
= $autoCloseIgnore
;
6131 * Get an ID of the body element, this can be used as the
6132 * `aria-describedby` attribute for an input field.
6134 * @return {string} The ID of the body element
6136 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
6137 var id
= this.$body
.attr( 'id' );
6138 if ( id
=== undefined ) {
6139 id
= OO
.ui
.generateElementId();
6140 this.$body
.attr( 'id', id
);
6146 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6147 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6148 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6149 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6155 * @param {Object} [config] Configuration options
6156 * @cfg {Object} [popup] Configuration to pass to popup
6157 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6159 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
6160 // Configuration initialization
6161 config
= config
|| {};
6164 this.popup
= new OO
.ui
.PopupWidget( $.extend(
6167 $floatableContainer
: this.$element
6171 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
6181 * @return {OO.ui.PopupWidget} Popup widget
6183 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
6188 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
6189 * which is used to display additional information or options.
6192 * // A PopupButtonWidget.
6193 * var popupButton = new OO.ui.PopupButtonWidget( {
6194 * label: 'Popup button with options',
6197 * $content: $( '<p>Additional options here.</p>' ),
6199 * align: 'force-left'
6202 * // Append the button to the DOM.
6203 * $( document.body ).append( popupButton.$element );
6206 * @extends OO.ui.ButtonWidget
6207 * @mixins OO.ui.mixin.PopupElement
6210 * @param {Object} [config] Configuration options
6211 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful
6212 * in cases where the expanded popup is larger than its containing `<div>`. The specified overlay
6213 * layer is usually on top of the containing `<div>` and has a larger area. By default, the popup
6214 * uses relative positioning.
6215 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
6217 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
6218 // Configuration initialization
6219 config
= config
|| {};
6221 // Parent constructor
6222 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
6224 // Mixin constructors
6225 OO
.ui
.mixin
.PopupElement
.call( this, config
);
6228 this.$overlay
= ( config
.$overlay
=== true ?
6229 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
6232 this.connect( this, {
6237 this.$element
.addClass( 'oo-ui-popupButtonWidget' );
6239 .addClass( 'oo-ui-popupButtonWidget-popup' )
6240 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6241 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6242 this.$overlay
.append( this.popup
.$element
);
6247 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
6248 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
6253 * Handle the button action being triggered.
6257 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
6258 this.popup
.toggle();
6262 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6264 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6269 * @mixins OO.ui.mixin.GroupElement
6272 * @param {Object} [config] Configuration options
6274 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
6275 // Mixin constructors
6276 OO
.ui
.mixin
.GroupElement
.call( this, config
);
6281 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
6286 * Set the disabled state of the widget.
6288 * This will also update the disabled state of child widgets.
6290 * @param {boolean} disabled Disable widget
6292 * @return {OO.ui.Widget} The widget, for chaining
6294 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
6298 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6299 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
6301 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6303 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6304 this.items
[ i
].updateDisabled();
6312 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6314 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group.
6315 * This allows bidirectional communication.
6317 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6325 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
6332 * Check if widget is disabled.
6334 * Checks parent if present, making disabled state inheritable.
6336 * @return {boolean} Widget is disabled
6338 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
6339 return this.disabled
||
6340 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
6344 * Set group element is in.
6346 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6348 * @return {OO.ui.Widget} The widget, for chaining
6350 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
6352 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6353 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
6355 // Initialize item disabled states
6356 this.updateDisabled();
6362 * OptionWidgets are special elements that can be selected and configured with data. The
6363 * data is often unique for each option, but it does not have to be. OptionWidgets are used
6364 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6365 * and examples, please see the [OOUI documentation on MediaWiki][1].
6367 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6370 * @extends OO.ui.Widget
6371 * @mixins OO.ui.mixin.ItemWidget
6372 * @mixins OO.ui.mixin.LabelElement
6373 * @mixins OO.ui.mixin.FlaggedElement
6374 * @mixins OO.ui.mixin.AccessKeyedElement
6375 * @mixins OO.ui.mixin.TitledElement
6378 * @param {Object} [config] Configuration options
6380 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
6381 // Configuration initialization
6382 config
= config
|| {};
6384 // Parent constructor
6385 OO
.ui
.OptionWidget
.parent
.call( this, config
);
6387 // Mixin constructors
6388 OO
.ui
.mixin
.ItemWidget
.call( this );
6389 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6390 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6391 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6392 OO
.ui
.mixin
.TitledElement
.call( this, config
);
6395 this.highlighted
= false;
6396 this.pressed
= false;
6397 this.setSelected( !!config
.selected
);
6401 .data( 'oo-ui-optionWidget', this )
6402 // Allow programmatic focussing (and by access key), but not tabbing
6403 .attr( 'tabindex', '-1' )
6404 .attr( 'role', 'option' )
6405 .addClass( 'oo-ui-optionWidget' )
6406 .append( this.$label
);
6411 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6412 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6413 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6414 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6415 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6416 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.TitledElement
);
6418 /* Static Properties */
6421 * Whether this option can be selected. See #setSelected.
6425 * @property {boolean}
6427 OO
.ui
.OptionWidget
.static.selectable
= true;
6430 * Whether this option can be highlighted. See #setHighlighted.
6434 * @property {boolean}
6436 OO
.ui
.OptionWidget
.static.highlightable
= true;
6439 * Whether this option can be pressed. See #setPressed.
6443 * @property {boolean}
6445 OO
.ui
.OptionWidget
.static.pressable
= true;
6448 * Whether this option will be scrolled into view when it is selected.
6452 * @property {boolean}
6454 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6459 * Check if the option can be selected.
6461 * @return {boolean} Item is selectable
6463 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6464 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6468 * Check if the option can be highlighted. A highlight indicates that the option
6469 * may be selected when a user presses Enter key or clicks. Disabled items cannot
6472 * @return {boolean} Item is highlightable
6474 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6475 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6479 * Check if the option can be pressed. The pressed state occurs when a user mouses
6480 * down on an item, but has not yet let go of the mouse.
6482 * @return {boolean} Item is pressable
6484 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6485 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6489 * Check if the option is selected.
6491 * @return {boolean} Item is selected
6493 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6494 return this.selected
;
6498 * Check if the option is highlighted. A highlight indicates that the
6499 * item may be selected when a user presses Enter key or clicks.
6501 * @return {boolean} Item is highlighted
6503 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6504 return this.highlighted
;
6508 * Check if the option is pressed. The pressed state occurs when a user mouses
6509 * down on an item, but has not yet let go of the mouse. The item may appear
6510 * selected, but it will not be selected until the user releases the mouse.
6512 * @return {boolean} Item is pressed
6514 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6515 return this.pressed
;
6519 * Set the option’s selected state. In general, all modifications to the selection
6520 * should be handled by the SelectWidget’s
6521 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
6523 * @param {boolean} [state=false] Select option
6525 * @return {OO.ui.Widget} The widget, for chaining
6527 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6528 if ( this.constructor.static.selectable
) {
6529 this.selected
= !!state
;
6531 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6532 .attr( 'aria-selected', state
.toString() );
6533 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6534 this.scrollElementIntoView();
6536 this.updateThemeClasses();
6542 * Set the option’s highlighted state. In general, all programmatic
6543 * modifications to the highlight should be handled by the
6544 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6545 * method instead of this method.
6547 * @param {boolean} [state=false] Highlight option
6549 * @return {OO.ui.Widget} The widget, for chaining
6551 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6552 if ( this.constructor.static.highlightable
) {
6553 this.highlighted
= !!state
;
6554 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6555 this.updateThemeClasses();
6561 * Set the option’s pressed state. In general, all
6562 * programmatic modifications to the pressed state should be handled by the
6563 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6564 * method instead of this method.
6566 * @param {boolean} [state=false] Press option
6568 * @return {OO.ui.Widget} The widget, for chaining
6570 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6571 if ( this.constructor.static.pressable
) {
6572 this.pressed
= !!state
;
6573 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6574 this.updateThemeClasses();
6580 * Get text to match search strings against.
6582 * The default implementation returns the label text, but subclasses
6583 * can override this to provide more complex behavior.
6585 * @return {string|boolean} String to match search string against
6587 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6588 var label
= this.getLabel();
6589 return typeof label
=== 'string' ? label
: this.$label
.text();
6593 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6594 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6595 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6598 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For
6599 * more information, please see the [OOUI documentation on MediaWiki][1].
6602 * // A select widget with three options.
6603 * var select = new OO.ui.SelectWidget( {
6605 * new OO.ui.OptionWidget( {
6607 * label: 'Option One',
6609 * new OO.ui.OptionWidget( {
6611 * label: 'Option Two',
6613 * new OO.ui.OptionWidget( {
6615 * label: 'Option Three',
6619 * $( document.body ).append( select.$element );
6621 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6625 * @extends OO.ui.Widget
6626 * @mixins OO.ui.mixin.GroupWidget
6629 * @param {Object} [config] Configuration options
6630 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6631 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6632 * the [OOUI documentation on MediaWiki] [2] for examples.
6633 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6634 * @cfg {boolean} [multiselect] Allow for multiple selections
6636 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6637 // Configuration initialization
6638 config
= config
|| {};
6640 // Parent constructor
6641 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6643 // Mixin constructors
6644 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {
6645 $group
: this.$element
6649 this.pressed
= false;
6650 this.selecting
= null;
6651 this.multiselect
= !!config
.multiselect
;
6652 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
6653 this.onDocumentMouseMoveHandler
= this.onDocumentMouseMove
.bind( this );
6654 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
6655 this.onDocumentKeyPressHandler
= this.onDocumentKeyPress
.bind( this );
6656 this.keyPressBuffer
= '';
6657 this.keyPressBufferTimer
= null;
6658 this.blockMouseOverEvents
= 0;
6661 this.connect( this, {
6665 focusin
: this.onFocus
.bind( this ),
6666 mousedown
: this.onMouseDown
.bind( this ),
6667 mouseover
: this.onMouseOver
.bind( this ),
6668 mouseleave
: this.onMouseLeave
.bind( this )
6673 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-unpressed' )
6674 .attr( 'role', 'listbox' );
6675 this.setFocusOwner( this.$element
);
6676 if ( Array
.isArray( config
.items
) ) {
6677 this.addItems( config
.items
);
6683 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6684 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6691 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6693 * @param {OO.ui.OptionWidget|null} item Highlighted item
6699 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6700 * pressed state of an option.
6702 * @param {OO.ui.OptionWidget|null} item Pressed item
6708 * A `select` event is emitted when the selection is modified programmatically with the #selectItem
6711 * @param {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} items Currently selected items
6717 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6719 * @param {OO.ui.OptionWidget} item Chosen item
6720 * @param {boolean} selected Item is selected
6726 * An `add` event is emitted when options are added to the select with the #addItems method.
6728 * @param {OO.ui.OptionWidget[]} items Added items
6729 * @param {number} index Index of insertion point
6735 * A `remove` event is emitted when options are removed from the select with the #clearItems
6736 * or #removeItems methods.
6738 * @param {OO.ui.OptionWidget[]} items Removed items
6741 /* Static methods */
6744 * Normalize text for filter matching
6746 * @param {string} text Text
6747 * @return {string} Normalized text
6749 OO
.ui
.SelectWidget
.static.normalizeForMatching = function ( text
) {
6750 // Replace trailing whitespace, normalize multiple spaces and make case insensitive
6751 var normalized
= text
.trim().replace( /\s+/, ' ' ).toLowerCase();
6753 // Normalize Unicode
6754 // eslint-disable-next-line no-restricted-properties
6755 if ( normalized
.normalize
) {
6756 // eslint-disable-next-line no-restricted-properties
6757 normalized
= normalized
.normalize();
6765 * Handle focus events
6768 * @param {jQuery.Event} event
6770 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6772 if ( event
.target
=== this.$element
[ 0 ] ) {
6773 // This widget was focussed, e.g. by the user tabbing to it.
6774 // The styles for focus state depend on one of the items being selected.
6775 if ( !this.findSelectedItem() ) {
6776 item
= this.findFirstSelectableItem();
6779 if ( event
.target
.tabIndex
=== -1 ) {
6780 // One of the options got focussed (and the event bubbled up here).
6781 // They can't be tabbed to, but they can be activated using access keys.
6782 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6783 item
= this.findTargetItem( event
);
6785 // There is something actually user-focusable in one of the labels of the options, and
6786 // the user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change
6793 if ( item
.constructor.static.highlightable
) {
6794 this.highlightItem( item
);
6796 this.selectItem( item
);
6800 if ( event
.target
!== this.$element
[ 0 ] ) {
6801 this.$focusOwner
.trigger( 'focus' );
6806 * Handle mouse down events.
6809 * @param {jQuery.Event} e Mouse down event
6810 * @return {undefined|boolean} False to prevent default if event is handled
6812 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6815 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6816 this.togglePressed( true );
6817 item
= this.findTargetItem( e
);
6818 if ( item
&& item
.isSelectable() ) {
6819 this.pressItem( item
);
6820 this.selecting
= item
;
6821 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6822 this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6829 * Handle document mouse up events.
6832 * @param {MouseEvent} e Mouse up event
6833 * @return {undefined|boolean} False to prevent default if event is handled
6835 OO
.ui
.SelectWidget
.prototype.onDocumentMouseUp = function ( e
) {
6838 this.togglePressed( false );
6839 if ( !this.selecting
) {
6840 item
= this.findTargetItem( e
);
6841 if ( item
&& item
.isSelectable() ) {
6842 this.selecting
= item
;
6845 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6846 this.pressItem( null );
6847 this.chooseItem( this.selecting
);
6848 this.selecting
= null;
6851 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6852 this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6858 * Handle document mouse move events.
6861 * @param {MouseEvent} e Mouse move event
6863 OO
.ui
.SelectWidget
.prototype.onDocumentMouseMove = function ( e
) {
6866 if ( !this.isDisabled() && this.pressed
) {
6867 item
= this.findTargetItem( e
);
6868 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6869 this.pressItem( item
);
6870 this.selecting
= item
;
6876 * Handle mouse over events.
6879 * @param {jQuery.Event} e Mouse over event
6880 * @return {undefined|boolean} False to prevent default if event is handled
6882 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6884 if ( this.blockMouseOverEvents
) {
6887 if ( !this.isDisabled() ) {
6888 item
= this.findTargetItem( e
);
6889 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6895 * Handle mouse leave events.
6898 * @param {jQuery.Event} e Mouse over event
6899 * @return {undefined|boolean} False to prevent default if event is handled
6901 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6902 if ( !this.isDisabled() ) {
6903 this.highlightItem( null );
6909 * Handle document key down events.
6912 * @param {KeyboardEvent} e Key down event
6914 OO
.ui
.SelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
6917 selected
= this.findSelectedItems(),
6918 currentItem
= this.findHighlightedItem() || (
6919 Array
.isArray( selected
) ? selected
[ 0 ] : selected
6921 firstItem
= this.getItems()[ 0 ];
6923 if ( !this.isDisabled() && this.isVisible() ) {
6924 switch ( e
.keyCode
) {
6925 case OO
.ui
.Keys
.ENTER
:
6926 if ( currentItem
) {
6927 // Was only highlighted, now let's select it. No-op if already selected.
6928 this.chooseItem( currentItem
);
6933 case OO
.ui
.Keys
.LEFT
:
6934 this.clearKeyPressBuffer();
6935 nextItem
= currentItem
?
6936 this.findRelativeSelectableItem( currentItem
, -1 ) : firstItem
;
6939 case OO
.ui
.Keys
.DOWN
:
6940 case OO
.ui
.Keys
.RIGHT
:
6941 this.clearKeyPressBuffer();
6942 nextItem
= currentItem
?
6943 this.findRelativeSelectableItem( currentItem
, 1 ) : firstItem
;
6946 case OO
.ui
.Keys
.ESCAPE
:
6947 case OO
.ui
.Keys
.TAB
:
6948 if ( currentItem
) {
6949 currentItem
.setHighlighted( false );
6951 this.unbindDocumentKeyDownListener();
6952 this.unbindDocumentKeyPressListener();
6953 // Don't prevent tabbing away / defocusing
6959 if ( nextItem
.constructor.static.highlightable
) {
6960 this.highlightItem( nextItem
);
6962 this.chooseItem( nextItem
);
6964 this.scrollItemIntoView( nextItem
);
6969 e
.stopPropagation();
6975 * Bind document key down listener.
6979 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyDownListener = function () {
6980 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6984 * Unbind document key down listener.
6988 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
6989 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6993 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6995 * @param {OO.ui.OptionWidget} item Item to scroll into view
6997 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6999 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic
7000 // scrolling and around 100-150 ms after it is finished.
7001 this.blockMouseOverEvents
++;
7002 item
.scrollElementIntoView().done( function () {
7003 setTimeout( function () {
7004 widget
.blockMouseOverEvents
--;
7010 * Clear the key-press buffer
7014 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
7015 if ( this.keyPressBufferTimer
) {
7016 clearTimeout( this.keyPressBufferTimer
);
7017 this.keyPressBufferTimer
= null;
7019 this.keyPressBuffer
= '';
7023 * Handle key press events.
7026 * @param {KeyboardEvent} e Key press event
7027 * @return {undefined|boolean} False to prevent default if event is handled
7029 OO
.ui
.SelectWidget
.prototype.onDocumentKeyPress = function ( e
) {
7030 var c
, filter
, item
, selected
;
7032 if ( !e
.charCode
) {
7033 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
7034 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
7039 // eslint-disable-next-line no-restricted-properties
7040 if ( String
.fromCodePoint
) {
7041 // eslint-disable-next-line no-restricted-properties
7042 c
= String
.fromCodePoint( e
.charCode
);
7044 c
= String
.fromCharCode( e
.charCode
);
7047 if ( this.keyPressBufferTimer
) {
7048 clearTimeout( this.keyPressBufferTimer
);
7050 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
7052 selected
= this.findSelectedItems();
7053 item
= this.findHighlightedItem() || (
7054 Array
.isArray( selected
) ? selected
[ 0 ] : selected
7057 if ( this.keyPressBuffer
=== c
) {
7058 // Common (if weird) special case: typing "xxxx" will cycle through all
7059 // the items beginning with "x".
7061 item
= this.findRelativeSelectableItem( item
, 1 );
7064 this.keyPressBuffer
+= c
;
7067 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
7068 if ( !item
|| !filter( item
) ) {
7069 item
= this.findRelativeSelectableItem( item
, 1, filter
);
7072 if ( this.isVisible() && item
.constructor.static.highlightable
) {
7073 this.highlightItem( item
);
7075 this.chooseItem( item
);
7077 this.scrollItemIntoView( item
);
7081 e
.stopPropagation();
7085 * Get a matcher for the specific string
7088 * @param {string} query String to match against items
7089 * @param {string} [mode='prefix'] Matching mode: 'substring', 'prefix', or 'exact'
7090 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
7092 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( query
, mode
) {
7093 var normalizeForMatching
= this.constructor.static.normalizeForMatching
,
7094 normalizedQuery
= normalizeForMatching( query
);
7096 // Support deprecated exact=true argument
7097 if ( mode
=== true ) {
7101 return function ( item
) {
7102 var matchText
= normalizeForMatching( item
.getMatchText() );
7104 if ( normalizedQuery
=== '' ) {
7105 // Empty string matches all, except if we are in 'exact'
7106 // mode, where it doesn't match at all
7107 return mode
!== 'exact';
7112 return matchText
=== normalizedQuery
;
7114 return matchText
.indexOf( normalizedQuery
) !== -1;
7117 return matchText
.indexOf( normalizedQuery
) === 0;
7123 * Bind document key press listener.
7127 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7128 this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
7132 * Unbind document key down listener.
7134 * If you override this, be sure to call this.clearKeyPressBuffer() from your
7139 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7140 this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
7141 this.clearKeyPressBuffer();
7145 * Visibility change handler
7148 * @param {boolean} visible
7150 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
7152 this.clearKeyPressBuffer();
7157 * Get the closest item to a jQuery.Event.
7160 * @param {jQuery.Event} e
7161 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
7163 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
7164 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
7165 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
7168 return $option
.data( 'oo-ui-optionWidget' ) || null;
7172 * Find all selected items, if there are any. If the widget allows for multiselect
7173 * it will return an array of selected options. If the widget doesn't allow for
7174 * multiselect, it will return the selected option or null if no item is selected.
7176 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
7177 * then return an array of selected items (or empty array),
7178 * if the widget is not multiselect, return a single selected item, or `null`
7179 * if no item is selected
7181 OO
.ui
.SelectWidget
.prototype.findSelectedItems = function () {
7182 var selected
= this.items
.filter( function ( item
) {
7183 return item
.isSelected();
7186 return this.multiselect
?
7188 selected
[ 0 ] || null;
7192 * Find selected item.
7194 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
7195 * then return an array of selected items (or empty array),
7196 * if the widget is not multiselect, return a single selected item, or `null`
7197 * if no item is selected
7199 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
7200 return this.findSelectedItems();
7204 * Find highlighted item.
7206 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
7208 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
7211 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7212 if ( this.items
[ i
].isHighlighted() ) {
7213 return this.items
[ i
];
7220 * Toggle pressed state.
7222 * Press is a state that occurs when a user mouses down on an item, but
7223 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
7224 * until the user releases the mouse.
7226 * @param {boolean} pressed An option is being pressed
7228 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
7229 if ( pressed
=== undefined ) {
7230 pressed
= !this.pressed
;
7232 if ( pressed
!== this.pressed
) {
7234 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
7235 .toggleClass( 'oo-ui-selectWidget-unpressed', !pressed
);
7236 this.pressed
= pressed
;
7241 * Highlight an option. If the `item` param is omitted, no options will be highlighted
7242 * and any existing highlight will be removed. The highlight is mutually exclusive.
7244 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
7247 * @return {OO.ui.Widget} The widget, for chaining
7249 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
7250 var i
, len
, highlighted
,
7253 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7254 highlighted
= this.items
[ i
] === item
;
7255 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
7256 this.items
[ i
].setHighlighted( highlighted
);
7262 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7264 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7266 this.emit( 'highlight', item
);
7273 * Fetch an item by its label.
7275 * @param {string} label Label of the item to select.
7276 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7277 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7279 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
7281 len
= this.items
.length
,
7282 filter
= this.getItemMatcher( label
, 'exact' );
7284 for ( i
= 0; i
< len
; i
++ ) {
7285 item
= this.items
[ i
];
7286 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7293 filter
= this.getItemMatcher( label
, 'prefix' );
7294 for ( i
= 0; i
< len
; i
++ ) {
7295 item
= this.items
[ i
];
7296 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7312 * Programmatically select an option by its label. If the item does not exist,
7313 * all options will be deselected.
7315 * @param {string} [label] Label of the item to select.
7316 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7319 * @return {OO.ui.Widget} The widget, for chaining
7321 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
7322 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
7323 if ( label
=== undefined || !itemFromLabel
) {
7324 return this.selectItem();
7326 return this.selectItem( itemFromLabel
);
7330 * Programmatically select an option by its data. If the `data` parameter is omitted,
7331 * or if the item does not exist, all options will be deselected.
7333 * @param {Object|string} [data] Value of the item to select, omit to deselect all
7336 * @return {OO.ui.Widget} The widget, for chaining
7338 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
7339 var itemFromData
= this.findItemFromData( data
);
7340 if ( data
=== undefined || !itemFromData
) {
7341 return this.selectItem();
7343 return this.selectItem( itemFromData
);
7347 * Programmatically unselect an option by its reference. If the widget
7348 * allows for multiple selections, there may be other items still selected;
7349 * otherwise, no items will be selected.
7350 * If no item is given, all selected items will be unselected.
7352 * @param {OO.ui.OptionWidget} [item] Item to unselect
7355 * @return {OO.ui.Widget} The widget, for chaining
7357 OO
.ui
.SelectWidget
.prototype.unselectItem = function ( item
) {
7359 item
.setSelected( false );
7361 this.items
.forEach( function ( item
) {
7362 item
.setSelected( false );
7366 this.emit( 'select', this.findSelectedItems() );
7371 * Programmatically select an option by its reference. If the `item` parameter is omitted,
7372 * all options will be deselected.
7374 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7377 * @return {OO.ui.Widget} The widget, for chaining
7379 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
7380 var i
, len
, selected
,
7383 if ( this.multiselect
&& item
) {
7384 // Select the item directly
7385 item
.setSelected( true );
7387 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7388 selected
= this.items
[ i
] === item
;
7389 if ( this.items
[ i
].isSelected() !== selected
) {
7390 this.items
[ i
].setSelected( selected
);
7396 // TODO: When should a non-highlightable element be selected?
7397 if ( item
&& !item
.constructor.static.highlightable
) {
7399 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7401 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7404 this.emit( 'select', this.findSelectedItems() );
7413 * Press is a state that occurs when a user mouses down on an item, but has not
7414 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7415 * releases the mouse.
7417 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7420 * @return {OO.ui.Widget} The widget, for chaining
7422 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
7423 var i
, len
, pressed
,
7426 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7427 pressed
= this.items
[ i
] === item
;
7428 if ( this.items
[ i
].isPressed() !== pressed
) {
7429 this.items
[ i
].setPressed( pressed
);
7434 this.emit( 'press', item
);
7443 * Note that ‘choose’ should never be modified programmatically. A user can choose
7444 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7445 * use the #selectItem method.
7447 * This method is identical to #selectItem, but may vary in subclasses that take additional action
7448 * when users choose an item with the keyboard or mouse.
7450 * @param {OO.ui.OptionWidget} item Item to choose
7453 * @return {OO.ui.Widget} The widget, for chaining
7455 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
7457 if ( this.multiselect
&& item
.isSelected() ) {
7458 this.unselectItem( item
);
7460 this.selectItem( item
);
7463 this.emit( 'choose', item
, item
.isSelected() );
7470 * Find an option by its position relative to the specified item (or to the start of the option
7471 * array, if item is `null`). The direction in which to search through the option array is specified
7472 * with a number: -1 for reverse (the default) or 1 for forward. The method will return an option,
7473 * or `null` if there are no options in the array.
7475 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at
7476 * the beginning of the array.
7477 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7478 * @param {Function} [filter] Only consider items for which this function returns
7479 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
7480 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
7482 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
7483 var currentIndex
, nextIndex
, i
,
7484 increase
= direction
> 0 ? 1 : -1,
7485 len
= this.items
.length
;
7487 if ( item
instanceof OO
.ui
.OptionWidget
) {
7488 currentIndex
= this.items
.indexOf( item
);
7489 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7491 // If no item is selected and moving forward, start at the beginning.
7492 // If moving backward, start at the end.
7493 nextIndex
= direction
> 0 ? 0 : len
- 1;
7496 for ( i
= 0; i
< len
; i
++ ) {
7497 item
= this.items
[ nextIndex
];
7499 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7500 ( !filter
|| filter( item
) )
7504 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7510 * Find the next selectable item or `null` if there are no selectable items.
7511 * Disabled options and menu-section markers and breaks are not selectable.
7513 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7515 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7516 return this.findRelativeSelectableItem( null, 1 );
7520 * Add an array of options to the select. Optionally, an index number can be used to
7521 * specify an insertion point.
7523 * @param {OO.ui.OptionWidget[]} items Items to add
7524 * @param {number} [index] Index to insert items after
7527 * @return {OO.ui.Widget} The widget, for chaining
7529 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7531 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7533 // Always provide an index, even if it was omitted
7534 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7540 * Remove the specified array of options from the select. Options will be detached
7541 * from the DOM, not removed, so they can be reused later. To remove all options from
7542 * the select, you may wish to use the #clearItems method instead.
7544 * @param {OO.ui.OptionWidget[]} items Items to remove
7547 * @return {OO.ui.Widget} The widget, for chaining
7549 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7552 // Deselect items being removed
7553 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7555 if ( item
.isSelected() ) {
7556 this.selectItem( null );
7561 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7563 this.emit( 'remove', items
);
7569 * Clear all options from the select. Options will be detached from the DOM, not removed,
7570 * so that they can be reused later. To remove a subset of options from the select, use
7571 * the #removeItems method.
7575 * @return {OO.ui.Widget} The widget, for chaining
7577 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7578 var items
= this.items
.slice();
7581 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7584 this.selectItem( null );
7586 this.emit( 'remove', items
);
7592 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7594 * This is used to set `aria-activedescendant` and `aria-expanded` on it.
7597 * @param {jQuery} $focusOwner
7599 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7600 this.$focusOwner
= $focusOwner
;
7604 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7605 * with an {@link OO.ui.mixin.IconElement icon} and/or
7606 * {@link OO.ui.mixin.IndicatorElement indicator}.
7607 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7608 * options. For more information about options and selects, please see the
7609 * [OOUI documentation on MediaWiki][1].
7612 * // Decorated options in a select widget.
7613 * var select = new OO.ui.SelectWidget( {
7615 * new OO.ui.DecoratedOptionWidget( {
7617 * label: 'Option with icon',
7620 * new OO.ui.DecoratedOptionWidget( {
7622 * label: 'Option with indicator',
7627 * $( document.body ).append( select.$element );
7629 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7632 * @extends OO.ui.OptionWidget
7633 * @mixins OO.ui.mixin.IconElement
7634 * @mixins OO.ui.mixin.IndicatorElement
7637 * @param {Object} [config] Configuration options
7639 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7640 // Parent constructor
7641 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7643 // Mixin constructors
7644 OO
.ui
.mixin
.IconElement
.call( this, config
);
7645 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7649 .addClass( 'oo-ui-decoratedOptionWidget' )
7650 .prepend( this.$icon
)
7651 .append( this.$indicator
);
7656 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7657 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7658 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7661 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7662 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7663 * the [OOUI documentation on MediaWiki] [1] for more information.
7665 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7668 * @extends OO.ui.DecoratedOptionWidget
7671 * @param {Object} [config] Configuration options
7673 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7674 // Parent constructor
7675 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7678 this.checkIcon
= new OO
.ui
.IconWidget( {
7680 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7685 .prepend( this.checkIcon
.$element
)
7686 .addClass( 'oo-ui-menuOptionWidget' );
7691 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7693 /* Static Properties */
7699 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7702 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to
7703 * group one or more related {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets
7704 * cannot be highlighted or selected.
7707 * var dropdown = new OO.ui.DropdownWidget( {
7710 * new OO.ui.MenuSectionOptionWidget( {
7713 * new OO.ui.MenuOptionWidget( {
7715 * label: 'Welsh Corgi'
7717 * new OO.ui.MenuOptionWidget( {
7719 * label: 'Standard Poodle'
7721 * new OO.ui.MenuSectionOptionWidget( {
7724 * new OO.ui.MenuOptionWidget( {
7731 * $( document.body ).append( dropdown.$element );
7734 * @extends OO.ui.DecoratedOptionWidget
7737 * @param {Object} [config] Configuration options
7739 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7740 // Parent constructor
7741 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7745 .addClass( 'oo-ui-menuSectionOptionWidget' )
7746 .removeAttr( 'role aria-selected' );
7747 this.selected
= false;
7752 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7754 /* Static Properties */
7760 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7766 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7769 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7770 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7771 * See {@link OO.ui.DropdownWidget DropdownWidget},
7772 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}, and
7773 * {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7774 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7775 * and customized to be opened, closed, and displayed as needed.
7777 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7778 * mouse outside the menu.
7780 * Menus also have support for keyboard interaction:
7782 * - Enter/Return key: choose and select a menu option
7783 * - Up-arrow key: highlight the previous menu option
7784 * - Down-arrow key: highlight the next menu option
7785 * - Escape key: hide the menu
7787 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7789 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7790 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7793 * @extends OO.ui.SelectWidget
7794 * @mixins OO.ui.mixin.ClippableElement
7795 * @mixins OO.ui.mixin.FloatableElement
7798 * @param {Object} [config] Configuration options
7799 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu
7800 * items that match the text the user types. This config is used by
7801 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget} and
7802 * {@link OO.ui.mixin.LookupElement LookupElement}
7803 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7804 * the text the user types. This config is used by
7805 * {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
7806 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks
7807 * the mouse anywhere on the page outside of this widget, the menu is hidden. For example, if
7808 * there is a button that toggles the menu's visibility on click, the menu will be hidden then
7809 * re-shown when the user clicks that button, unless the button (or its parent widget) is passed
7811 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7812 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7813 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7814 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7815 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7816 * @cfg {string} [filterMode='prefix'] The mode by which the menu filters the results.
7817 * Options are 'exact', 'prefix' or 'substring'. See `OO.ui.SelectWidget#getItemMatcher`
7818 * @cfg {number|string} [width] Width of the menu as a number of pixels or CSS string with unit
7819 * suffix, used by {@link OO.ui.mixin.ClippableElement ClippableElement}
7821 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7822 // Configuration initialization
7823 config
= config
|| {};
7825 // Parent constructor
7826 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7828 // Mixin constructors
7829 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( { $clippable
: this.$group
}, config
) );
7830 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7832 // Initial vertical positions other than 'center' will result in
7833 // the menu being flipped if there is not enough space in the container.
7834 // Store the original position so we know what to reset to.
7835 this.originalVerticalPosition
= this.verticalPosition
;
7838 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7839 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7840 this.filterFromInput
= !!config
.filterFromInput
;
7841 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7842 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7843 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7844 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7845 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7846 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7847 this.lastHighlightedItem
= null;
7848 this.width
= config
.width
;
7849 this.filterMode
= config
.filterMode
;
7852 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7853 if ( config
.widget
) {
7854 this.setFocusOwner( config
.widget
.$tabIndexed
);
7857 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7858 // that reference properties not initialized at that time of parent class construction
7859 // TODO: Find a better way to handle post-constructor setup
7860 this.visible
= false;
7861 this.$element
.addClass( 'oo-ui-element-hidden' );
7862 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7867 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7868 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7869 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7876 * The menu is ready: it is visible and has been positioned and clipped.
7879 /* Static properties */
7882 * Positions to flip to if there isn't room in the container for the
7883 * menu in a specific direction.
7885 * @property {Object.<string,string>}
7887 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7897 * Handles document mouse down events.
7900 * @param {MouseEvent} e Mouse down event
7902 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7906 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7911 this.toggle( false );
7918 OO
.ui
.MenuSelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
7919 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7921 if ( !this.isDisabled() && this.isVisible() ) {
7922 switch ( e
.keyCode
) {
7923 case OO
.ui
.Keys
.LEFT
:
7924 case OO
.ui
.Keys
.RIGHT
:
7925 // Do nothing if a text field is associated, arrow keys will be handled natively
7926 if ( !this.$input
) {
7927 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7930 case OO
.ui
.Keys
.ESCAPE
:
7931 case OO
.ui
.Keys
.TAB
:
7932 if ( currentItem
&& !this.multiselect
) {
7933 currentItem
.setHighlighted( false );
7935 this.toggle( false );
7936 // Don't prevent tabbing away, prevent defocusing
7937 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7939 e
.stopPropagation();
7943 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7950 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7951 * or after items were added/removed (always).
7955 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7956 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7958 len
= this.items
.length
,
7959 showAll
= !this.isVisible(),
7962 if ( this.$input
&& this.filterFromInput
) {
7963 filter
= showAll
? null : this.getItemMatcher( this.$input
.val(), this.filterMode
);
7964 exactFilter
= this.getItemMatcher( this.$input
.val(), 'exact' );
7965 // Hide non-matching options, and also hide section headers if all options
7966 // in their section are hidden.
7967 for ( i
= 0; i
< len
; i
++ ) {
7968 item
= this.items
[ i
];
7969 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7971 // If the previous section was empty, hide its header
7972 section
.toggle( showAll
|| !sectionEmpty
);
7975 sectionEmpty
= true;
7976 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7977 visible
= showAll
|| filter( item
);
7978 exactMatch
= exactMatch
|| exactFilter( item
);
7979 anyVisible
= anyVisible
|| visible
;
7980 sectionEmpty
= sectionEmpty
&& !visible
;
7981 item
.toggle( visible
);
7984 // Process the final section
7986 section
.toggle( showAll
|| !sectionEmpty
);
7989 if ( !anyVisible
) {
7990 this.highlightItem( null );
7993 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7996 this.highlightOnFilter
&&
7997 !( this.lastHighlightedItem
&& this.lastHighlightedItem
.isVisible() ) &&
8000 // Highlight the first item on the list
8002 items
= this.getItems();
8003 for ( i
= 0; i
< items
.length
; i
++ ) {
8004 if ( items
[ i
].isVisible() ) {
8009 this.highlightItem( item
);
8010 this.lastHighlightedItem
= item
;
8014 // Reevaluate clipping
8021 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyDownListener = function () {
8022 if ( this.$input
) {
8023 this.$input
.on( 'keydown', this.onDocumentKeyDownHandler
);
8025 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyDownListener
.call( this );
8032 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
8033 if ( this.$input
) {
8034 this.$input
.off( 'keydown', this.onDocumentKeyDownHandler
);
8036 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyDownListener
.call( this );
8043 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyPressListener = function () {
8044 if ( this.$input
) {
8045 if ( this.filterFromInput
) {
8047 'keydown mouseup cut paste change input select',
8048 this.onInputEditHandler
8050 this.updateItemVisibility();
8053 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyPressListener
.call( this );
8060 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
8061 if ( this.$input
) {
8062 if ( this.filterFromInput
) {
8064 'keydown mouseup cut paste change input select',
8065 this.onInputEditHandler
8067 this.updateItemVisibility();
8070 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyPressListener
.call( this );
8077 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is
8080 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with
8081 * the keyboard or mouse and it becomes selected. To select an item programmatically,
8082 * use the #selectItem method.
8084 * @param {OO.ui.OptionWidget} item Item to choose
8086 * @return {OO.ui.Widget} The widget, for chaining
8088 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
8089 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
8090 if ( this.hideOnChoose
) {
8091 this.toggle( false );
8099 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
8101 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
8103 this.updateItemVisibility();
8111 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
8113 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
8115 this.updateItemVisibility();
8123 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
8125 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
8127 this.updateItemVisibility();
8133 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
8134 * `.toggle( true )` after its #$element is attached to the DOM.
8136 * Do not show the menu while it is not attached to the DOM. The calculations required to display
8137 * it in the right place and with the right dimensions only work correctly while it is attached.
8138 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
8139 * strictly enforced, so currently it only generates a warning in the browser console.
8144 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
8145 var change
, originalHeight
, flippedHeight
, selectedItem
;
8147 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
8148 change
= visible
!== this.isVisible();
8150 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
8151 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
8152 this.warnedUnattached
= true;
8155 if ( change
&& visible
) {
8156 // Reset position before showing the popup again. It's possible we no longer need to flip
8157 // (e.g. if the user scrolled).
8158 this.setVerticalPosition( this.originalVerticalPosition
);
8162 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
8168 this.setIdealSize( this.width
);
8169 } else if ( this.$floatableContainer
) {
8170 this.$clippable
.css( 'width', 'auto' );
8172 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
8173 // Dropdown is smaller than handle so expand to width
8174 this.$floatableContainer
[ 0 ].offsetWidth
:
8175 // Dropdown is larger than handle so auto size
8178 this.$clippable
.css( 'width', '' );
8181 this.togglePositioning( !!this.$floatableContainer
);
8182 this.toggleClipping( true );
8184 this.bindDocumentKeyDownListener();
8185 this.bindDocumentKeyPressListener();
8188 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
8189 this.originalVerticalPosition
!== 'center'
8191 // If opening the menu in one direction causes it to be clipped, flip it
8192 originalHeight
= this.$element
.height();
8193 this.setVerticalPosition(
8194 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
8196 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
8197 // If flipping also causes it to be clipped, open in whichever direction
8198 // we have more space
8199 flippedHeight
= this.$element
.height();
8200 if ( originalHeight
> flippedHeight
) {
8201 this.setVerticalPosition( this.originalVerticalPosition
);
8205 // Note that we do not flip the menu's opening direction if the clipping changes
8206 // later (e.g. after the user scrolls), that seems like it would be annoying
8208 this.$focusOwner
.attr( 'aria-expanded', 'true' );
8210 selectedItem
= this.findSelectedItem();
8211 if ( !this.multiselect
&& selectedItem
) {
8212 // TODO: Verify if this is even needed; This is already done on highlight changes
8213 // in SelectWidget#highlightItem, so we should just need to highlight the item
8214 // we need to highlight here and not bother with attr or checking selections.
8215 this.$focusOwner
.attr( 'aria-activedescendant', selectedItem
.getElementId() );
8216 selectedItem
.scrollElementIntoView( { duration
: 0 } );
8220 if ( this.autoHide
) {
8221 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8224 this.emit( 'ready' );
8226 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
8227 this.unbindDocumentKeyDownListener();
8228 this.unbindDocumentKeyPressListener();
8229 this.$focusOwner
.attr( 'aria-expanded', 'false' );
8230 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8231 this.togglePositioning( false );
8232 this.toggleClipping( false );
8233 this.lastHighlightedItem
= null;
8241 * Scroll to the top of the menu
8243 OO
.ui
.MenuSelectWidget
.prototype.scrollToTop = function () {
8244 this.$element
.scrollTop( 0 );
8248 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
8249 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
8250 * users can interact with it.
8252 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8253 * OO.ui.DropdownInputWidget instead.
8256 * // A DropdownWidget with a menu that contains three options.
8257 * var dropDown = new OO.ui.DropdownWidget( {
8258 * label: 'Dropdown menu: Select a menu option',
8261 * new OO.ui.MenuOptionWidget( {
8265 * new OO.ui.MenuOptionWidget( {
8269 * new OO.ui.MenuOptionWidget( {
8277 * $( document.body ).append( dropDown.$element );
8279 * dropDown.getMenu().selectItemByData( 'b' );
8281 * dropDown.getMenu().findSelectedItem().getData(); // Returns 'b'.
8283 * For more information, please see the [OOUI documentation on MediaWiki] [1].
8285 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8288 * @extends OO.ui.Widget
8289 * @mixins OO.ui.mixin.IconElement
8290 * @mixins OO.ui.mixin.IndicatorElement
8291 * @mixins OO.ui.mixin.LabelElement
8292 * @mixins OO.ui.mixin.TitledElement
8293 * @mixins OO.ui.mixin.TabIndexedElement
8296 * @param {Object} [config] Configuration options
8297 * @cfg {Object} [menu] Configuration options to pass to
8298 * {@link OO.ui.MenuSelectWidget menu select widget}.
8299 * @cfg {jQuery|boolean} [$overlay] Render the menu into a separate layer. This configuration is
8300 * useful in cases where the expanded menu is larger than its containing `<div>`. The specified
8301 * overlay layer is usually on top of the containing `<div>` and has a larger area. By default,
8302 * the menu uses relative positioning. Pass 'true' to use the default overlay.
8303 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
8305 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
8306 // Configuration initialization
8307 config
= $.extend( { indicator
: 'down' }, config
);
8309 // Parent constructor
8310 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
8312 // Properties (must be set before TabIndexedElement constructor call)
8313 this.$handle
= $( '<span>' );
8314 this.$overlay
= ( config
.$overlay
=== true ?
8315 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
8317 // Mixin constructors
8318 OO
.ui
.mixin
.IconElement
.call( this, config
);
8319 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8320 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8321 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
8322 $titled
: this.$label
8324 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
8325 $tabIndexed
: this.$handle
8329 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
8331 $floatableContainer
: this.$element
8336 click
: this.onClick
.bind( this ),
8337 keydown
: this.onKeyDown
.bind( this ),
8338 // Hack? Handle type-to-search when menu is not expanded and not handling its own events.
8339 keypress
: this.menu
.onDocumentKeyPressHandler
,
8340 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
8342 this.menu
.connect( this, {
8343 select
: 'onMenuSelect',
8344 toggle
: 'onMenuToggle'
8351 'aria-readonly': 'true'
8354 .addClass( 'oo-ui-dropdownWidget-handle' )
8355 .append( this.$icon
, this.$label
, this.$indicator
)
8358 'aria-autocomplete': 'list',
8359 'aria-expanded': 'false',
8360 'aria-haspopup': 'true',
8361 'aria-owns': this.menu
.getElementId()
8364 .addClass( 'oo-ui-dropdownWidget' )
8365 .append( this.$handle
);
8366 this.$overlay
.append( this.menu
.$element
);
8371 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8372 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8373 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8374 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8375 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8376 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8383 * @return {OO.ui.MenuSelectWidget} Menu of widget
8385 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8390 * Handles menu select events.
8393 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8395 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8399 this.setLabel( null );
8403 selectedLabel
= item
.getLabel();
8405 // If the label is a DOM element, clone it, because setLabel will append() it
8406 if ( selectedLabel
instanceof $ ) {
8407 selectedLabel
= selectedLabel
.clone();
8410 this.setLabel( selectedLabel
);
8414 * Handle menu toggle events.
8417 * @param {boolean} isVisible Open state of the menu
8419 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8420 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8424 * Handle mouse click events.
8427 * @param {jQuery.Event} e Mouse click event
8428 * @return {undefined|boolean} False to prevent default if event is handled
8430 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8431 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8438 * Handle key down events.
8441 * @param {jQuery.Event} e Key down event
8442 * @return {undefined|boolean} False to prevent default if event is handled
8444 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8446 !this.isDisabled() &&
8448 e
.which
=== OO
.ui
.Keys
.ENTER
||
8450 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8451 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8452 // Space only closes the menu is the user is not typing to search.
8453 this.menu
.keyPressBuffer
=== ''
8456 !this.menu
.isVisible() &&
8458 e
.which
=== OO
.ui
.Keys
.UP
||
8459 e
.which
=== OO
.ui
.Keys
.DOWN
8470 * RadioOptionWidget is an option widget that looks like a radio button.
8471 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8472 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8474 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8477 * @extends OO.ui.OptionWidget
8480 * @param {Object} [config] Configuration options
8482 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8483 // Configuration initialization
8484 config
= config
|| {};
8486 // Properties (must be done before parent constructor which calls #setDisabled)
8487 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8489 // Parent constructor
8490 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8493 // Remove implicit role, we're handling it ourselves
8494 this.radio
.$input
.attr( 'role', 'presentation' );
8496 .addClass( 'oo-ui-radioOptionWidget' )
8497 .attr( 'role', 'radio' )
8498 .attr( 'aria-checked', 'false' )
8499 .removeAttr( 'aria-selected' )
8500 .prepend( this.radio
.$element
);
8505 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8507 /* Static Properties */
8513 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8519 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8525 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8531 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8538 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8539 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8541 this.radio
.setSelected( state
);
8543 .attr( 'aria-checked', state
.toString() )
8544 .removeAttr( 'aria-selected' );
8552 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8553 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8555 this.radio
.setDisabled( this.isDisabled() );
8561 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8562 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8563 * an interface for adding, removing and selecting options.
8564 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8566 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8567 * OO.ui.RadioSelectInputWidget instead.
8570 * // A RadioSelectWidget with RadioOptions.
8571 * var option1 = new OO.ui.RadioOptionWidget( {
8573 * label: 'Selected radio option'
8575 * option2 = new OO.ui.RadioOptionWidget( {
8577 * label: 'Unselected radio option'
8579 * radioSelect = new OO.ui.RadioSelectWidget( {
8580 * items: [ option1, option2 ]
8583 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8584 * radioSelect.selectItem( option1 );
8586 * $( document.body ).append( radioSelect.$element );
8588 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8592 * @extends OO.ui.SelectWidget
8593 * @mixins OO.ui.mixin.TabIndexedElement
8596 * @param {Object} [config] Configuration options
8598 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8599 // Parent constructor
8600 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8602 // Mixin constructors
8603 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8607 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8608 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8613 .addClass( 'oo-ui-radioSelectWidget' )
8614 .attr( 'role', 'radiogroup' );
8619 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8620 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8623 * MultioptionWidgets are special elements that can be selected and configured with data. The
8624 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8625 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8626 * and examples, please see the [OOUI documentation on MediaWiki][1].
8628 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8631 * @extends OO.ui.Widget
8632 * @mixins OO.ui.mixin.ItemWidget
8633 * @mixins OO.ui.mixin.LabelElement
8634 * @mixins OO.ui.mixin.TitledElement
8637 * @param {Object} [config] Configuration options
8638 * @cfg {boolean} [selected=false] Whether the option is initially selected
8640 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8641 // Configuration initialization
8642 config
= config
|| {};
8644 // Parent constructor
8645 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8647 // Mixin constructors
8648 OO
.ui
.mixin
.ItemWidget
.call( this );
8649 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8650 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8653 this.selected
= null;
8657 .addClass( 'oo-ui-multioptionWidget' )
8658 .append( this.$label
);
8659 this.setSelected( config
.selected
);
8664 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8665 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8666 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8667 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.TitledElement
);
8674 * A change event is emitted when the selected state of the option changes.
8676 * @param {boolean} selected Whether the option is now selected
8682 * Check if the option is selected.
8684 * @return {boolean} Item is selected
8686 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8687 return this.selected
;
8691 * Set the option’s selected state. In general, all modifications to the selection
8692 * should be handled by the SelectWidget’s
8693 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
8695 * @param {boolean} [state=false] Select option
8697 * @return {OO.ui.Widget} The widget, for chaining
8699 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8701 if ( this.selected
!== state
) {
8702 this.selected
= state
;
8703 this.emit( 'change', state
);
8704 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8710 * MultiselectWidget allows selecting multiple options from a list.
8712 * For more information about menus and options, please see the [OOUI documentation
8715 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8719 * @extends OO.ui.Widget
8720 * @mixins OO.ui.mixin.GroupWidget
8721 * @mixins OO.ui.mixin.TitledElement
8724 * @param {Object} [config] Configuration options
8725 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8727 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8728 // Parent constructor
8729 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8731 // Configuration initialization
8732 config
= config
|| {};
8734 // Mixin constructors
8735 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8736 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8742 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8743 // by GroupElement only when items are added/removed
8744 this.connect( this, {
8745 select
: [ 'emit', 'change' ]
8749 if ( config
.items
) {
8750 this.addItems( config
.items
);
8752 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8753 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8754 .append( this.$group
);
8759 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8760 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8761 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.TitledElement
);
8768 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8774 * A select event is emitted when an item is selected or deselected.
8780 * Find options that are selected.
8782 * @return {OO.ui.MultioptionWidget[]} Selected options
8784 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8785 return this.items
.filter( function ( item
) {
8786 return item
.isSelected();
8791 * Find the data of options that are selected.
8793 * @return {Object[]|string[]} Values of selected options
8795 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8796 return this.findSelectedItems().map( function ( item
) {
8802 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8804 * @param {OO.ui.MultioptionWidget[]} items Items to select
8806 * @return {OO.ui.Widget} The widget, for chaining
8808 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8809 this.items
.forEach( function ( item
) {
8810 var selected
= items
.indexOf( item
) !== -1;
8811 item
.setSelected( selected
);
8817 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8819 * @param {Object[]|string[]} datas Values of items to select
8821 * @return {OO.ui.Widget} The widget, for chaining
8823 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8826 items
= datas
.map( function ( data
) {
8827 return widget
.findItemFromData( data
);
8829 this.selectItems( items
);
8834 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8835 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8836 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8838 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8841 * @extends OO.ui.MultioptionWidget
8844 * @param {Object} [config] Configuration options
8846 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8847 // Configuration initialization
8848 config
= config
|| {};
8850 // Properties (must be done before parent constructor which calls #setDisabled)
8851 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8853 // Parent constructor
8854 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8857 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8858 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8862 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8863 .prepend( this.checkbox
.$element
);
8868 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8870 /* Static Properties */
8876 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8881 * Handle checkbox selected state change.
8885 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8886 this.setSelected( this.checkbox
.isSelected() );
8892 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8893 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8894 this.checkbox
.setSelected( state
);
8901 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8902 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8903 this.checkbox
.setDisabled( this.isDisabled() );
8910 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8911 this.checkbox
.focus();
8915 * Handle key down events.
8918 * @param {jQuery.Event} e
8920 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8922 element
= this.getElementGroup(),
8925 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8926 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8927 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8928 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8938 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8939 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8940 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8941 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8943 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8944 * OO.ui.CheckboxMultiselectInputWidget instead.
8947 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8948 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8951 * label: 'Selected checkbox'
8953 * option2 = new OO.ui.CheckboxMultioptionWidget( {
8955 * label: 'Unselected checkbox'
8957 * multiselect = new OO.ui.CheckboxMultiselectWidget( {
8958 * items: [ option1, option2 ]
8960 * $( document.body ).append( multiselect.$element );
8962 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8965 * @extends OO.ui.MultiselectWidget
8968 * @param {Object} [config] Configuration options
8970 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8971 // Parent constructor
8972 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8975 this.$lastClicked
= null;
8978 this.$group
.on( 'click', this.onClick
.bind( this ) );
8981 this.$element
.addClass( 'oo-ui-checkboxMultiselectWidget' );
8986 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8991 * Get an option by its position relative to the specified item (or to the start of the
8992 * option array, if item is `null`). The direction in which to search through the option array
8993 * is specified with a number: -1 for reverse (the default) or 1 for forward. The method will
8994 * return an option, or `null` if there are no options in the array.
8996 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or
8997 * `null` to start at the beginning of the array.
8998 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8999 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items
9002 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
9003 var currentIndex
, nextIndex
, i
,
9004 increase
= direction
> 0 ? 1 : -1,
9005 len
= this.items
.length
;
9008 currentIndex
= this.items
.indexOf( item
);
9009 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
9011 // If no item is selected and moving forward, start at the beginning.
9012 // If moving backward, start at the end.
9013 nextIndex
= direction
> 0 ? 0 : len
- 1;
9016 for ( i
= 0; i
< len
; i
++ ) {
9017 item
= this.items
[ nextIndex
];
9018 if ( item
&& !item
.isDisabled() ) {
9021 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
9027 * Handle click events on checkboxes.
9029 * @param {jQuery.Event} e
9031 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
9032 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
9033 $lastClicked
= this.$lastClicked
,
9034 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
9035 .not( '.oo-ui-widget-disabled' );
9037 // Allow selecting multiple options at once by Shift-clicking them
9038 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
9039 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
9040 lastClickedIndex
= $options
.index( $lastClicked
);
9041 nowClickedIndex
= $options
.index( $nowClicked
);
9042 // If it's the same item, either the user is being silly, or it's a fake event generated
9043 // by the browser. In either case we don't need custom handling.
9044 if ( nowClickedIndex
!== lastClickedIndex
) {
9046 wasSelected
= items
[ nowClickedIndex
].isSelected();
9047 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
9049 // This depends on the DOM order of the items and the order of the .items array being
9051 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
9052 if ( !items
[ i
].isDisabled() ) {
9053 items
[ i
].setSelected( !wasSelected
);
9056 // For the now-clicked element, use immediate timeout to allow the browser to do its own
9057 // handling first, then set our value. The order in which events happen is different for
9058 // clicks on the <input> and on the <label> and there are additional fake clicks fired
9059 // for non-click actions that change the checkboxes.
9061 setTimeout( function () {
9062 if ( !items
[ nowClickedIndex
].isDisabled() ) {
9063 items
[ nowClickedIndex
].setSelected( !wasSelected
);
9069 if ( $nowClicked
.length
) {
9070 this.$lastClicked
= $nowClicked
;
9078 * @return {OO.ui.Widget} The widget, for chaining
9080 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
9082 if ( !this.isDisabled() ) {
9083 item
= this.getRelativeFocusableItem( null, 1 );
9094 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
9099 * Progress bars visually display the status of an operation, such as a download,
9100 * and can be either determinate or indeterminate:
9102 * - **determinate** process bars show the percent of an operation that is complete.
9104 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
9105 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
9106 * not use percentages.
9108 * The value of the `progress` configuration determines whether the bar is determinate
9112 * // Examples of determinate and indeterminate progress bars.
9113 * var progressBar1 = new OO.ui.ProgressBarWidget( {
9116 * var progressBar2 = new OO.ui.ProgressBarWidget();
9118 * // Create a FieldsetLayout to layout progress bars.
9119 * var fieldset = new OO.ui.FieldsetLayout;
9120 * fieldset.addItems( [
9121 * new OO.ui.FieldLayout( progressBar1, {
9122 * label: 'Determinate',
9125 * new OO.ui.FieldLayout( progressBar2, {
9126 * label: 'Indeterminate',
9130 * $( document.body ).append( fieldset.$element );
9133 * @extends OO.ui.Widget
9136 * @param {Object} [config] Configuration options
9137 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
9138 * To create a determinate progress bar, specify a number that reflects the initial
9140 * By default, the progress bar is indeterminate.
9142 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
9143 // Configuration initialization
9144 config
= config
|| {};
9146 // Parent constructor
9147 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
9150 this.$bar
= $( '<div>' );
9151 this.progress
= null;
9154 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
9155 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
9158 role
: 'progressbar',
9160 'aria-valuemax': 100
9162 .addClass( 'oo-ui-progressBarWidget' )
9163 .append( this.$bar
);
9168 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
9170 /* Static Properties */
9176 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
9181 * Get the percent of the progress that has been completed. Indeterminate progresses will
9184 * @return {number|boolean} Progress percent
9186 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
9187 return this.progress
;
9191 * Set the percent of the process completed or `false` for an indeterminate process.
9193 * @param {number|boolean} progress Progress percent or `false` for indeterminate
9195 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
9196 this.progress
= progress
;
9198 if ( progress
!== false ) {
9199 this.$bar
.css( 'width', this.progress
+ '%' );
9200 this.$element
.attr( 'aria-valuenow', this.progress
);
9202 this.$bar
.css( 'width', '' );
9203 this.$element
.removeAttr( 'aria-valuenow' );
9205 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
9209 * InputWidget is the base class for all input widgets, which
9210 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox
9211 * inputs}, {@link OO.ui.RadioInputWidget radio inputs}, and
9212 * {@link OO.ui.ButtonInputWidget button inputs}.
9213 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
9215 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9219 * @extends OO.ui.Widget
9220 * @mixins OO.ui.mixin.TabIndexedElement
9221 * @mixins OO.ui.mixin.TitledElement
9222 * @mixins OO.ui.mixin.AccessKeyedElement
9225 * @param {Object} [config] Configuration options
9226 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9227 * @cfg {string} [value=''] The value of the input.
9228 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
9229 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
9230 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the
9231 * value of an input before it is accepted.
9233 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
9234 // Configuration initialization
9235 config
= config
|| {};
9237 // Parent constructor
9238 OO
.ui
.InputWidget
.parent
.call( this, config
);
9241 // See #reusePreInfuseDOM about config.$input
9242 this.$input
= config
.$input
|| this.getInputElement( config
);
9244 this.inputFilter
= config
.inputFilter
;
9246 // Mixin constructors
9247 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
9248 $tabIndexed
: this.$input
9250 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
9251 $titled
: this.$input
9253 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
9254 $accessKeyed
: this.$input
9258 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
9262 .addClass( 'oo-ui-inputWidget-input' )
9263 .attr( 'name', config
.name
)
9264 .prop( 'disabled', this.isDisabled() );
9266 .addClass( 'oo-ui-inputWidget' )
9267 .append( this.$input
);
9268 this.setValue( config
.value
);
9270 this.setDir( config
.dir
);
9272 if ( config
.inputId
!== undefined ) {
9273 this.setInputId( config
.inputId
);
9279 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
9280 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
9281 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
9282 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
9284 /* Static Methods */
9289 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9290 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9291 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
9292 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
9299 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9300 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9301 if ( config
.$input
&& config
.$input
.length
) {
9302 state
.value
= config
.$input
.val();
9303 // Might be better in TabIndexedElement, but it's awkward to do there because
9304 // mixins are awkward
9305 state
.focus
= config
.$input
.is( ':focus' );
9315 * A change event is emitted when the value of the input changes.
9317 * @param {string} value
9323 * Get input element.
9325 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9326 * different circumstances. The element must have a `value` property (like form elements).
9329 * @param {Object} config Configuration options
9330 * @return {jQuery} Input element
9332 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
9333 return $( '<input>' );
9337 * Handle potentially value-changing events.
9340 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9342 OO
.ui
.InputWidget
.prototype.onEdit = function () {
9344 if ( !this.isDisabled() ) {
9345 // Allow the stack to clear so the value will be updated
9346 setTimeout( function () {
9347 widget
.setValue( widget
.$input
.val() );
9353 * Get the value of the input.
9355 * @return {string} Input value
9357 OO
.ui
.InputWidget
.prototype.getValue = function () {
9358 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9359 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9360 var value
= this.$input
.val();
9361 if ( this.value
!== value
) {
9362 this.setValue( value
);
9368 * Set the directionality of the input.
9370 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9372 * @return {OO.ui.Widget} The widget, for chaining
9374 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9375 this.$input
.prop( 'dir', dir
);
9380 * Set the value of the input.
9382 * @param {string} value New value
9385 * @return {OO.ui.Widget} The widget, for chaining
9387 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9388 value
= this.cleanUpValue( value
);
9389 // Update the DOM if it has changed. Note that with cleanUpValue, it
9390 // is possible for the DOM value to change without this.value changing.
9391 if ( this.$input
.val() !== value
) {
9392 this.$input
.val( value
);
9394 if ( this.value
!== value
) {
9396 this.emit( 'change', this.value
);
9398 // The first time that the value is set (probably while constructing the widget),
9399 // remember it in defaultValue. This property can be later used to check whether
9400 // the value of the input has been changed since it was created.
9401 if ( this.defaultValue
=== undefined ) {
9402 this.defaultValue
= this.value
;
9403 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9409 * Clean up incoming value.
9411 * Ensures value is a string, and converts undefined and null to empty string.
9414 * @param {string} value Original value
9415 * @return {string} Cleaned up value
9417 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9418 if ( value
=== undefined || value
=== null ) {
9420 } else if ( this.inputFilter
) {
9421 return this.inputFilter( String( value
) );
9423 return String( value
);
9430 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9431 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9432 if ( this.$input
) {
9433 this.$input
.prop( 'disabled', this.isDisabled() );
9439 * Set the 'id' attribute of the `<input>` element.
9441 * @param {string} id
9443 * @return {OO.ui.Widget} The widget, for chaining
9445 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9446 this.$input
.attr( 'id', id
);
9453 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9454 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9455 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9456 this.setValue( state
.value
);
9458 if ( state
.focus
) {
9464 * Data widget intended for creating `<input type="hidden">` inputs.
9467 * @extends OO.ui.Widget
9470 * @param {Object} [config] Configuration options
9471 * @cfg {string} [value=''] The value of the input.
9472 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9474 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9475 // Configuration initialization
9476 config
= $.extend( { value
: '', name
: '' }, config
);
9478 // Parent constructor
9479 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9482 this.$element
.attr( {
9484 value
: config
.value
,
9487 this.$element
.removeAttr( 'aria-disabled' );
9492 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9494 /* Static Properties */
9500 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9503 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9504 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9505 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9506 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9507 * [OOUI documentation on MediaWiki] [1] for more information.
9510 * // A ButtonInputWidget rendered as an HTML button, the default.
9511 * var button = new OO.ui.ButtonInputWidget( {
9512 * label: 'Input button',
9516 * $( document.body ).append( button.$element );
9518 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9521 * @extends OO.ui.InputWidget
9522 * @mixins OO.ui.mixin.ButtonElement
9523 * @mixins OO.ui.mixin.IconElement
9524 * @mixins OO.ui.mixin.IndicatorElement
9525 * @mixins OO.ui.mixin.LabelElement
9526 * @mixins OO.ui.mixin.FlaggedElement
9529 * @param {Object} [config] Configuration options
9530 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute:
9531 * 'button', 'submit' or 'reset'.
9532 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9533 * Widgets configured to be an `<input>` do not support {@link #icon icons} and
9534 * {@link #indicator indicators},
9535 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should
9536 * only be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9538 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9539 // Configuration initialization
9540 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9542 // See InputWidget#reusePreInfuseDOM about config.$input
9543 if ( config
.$input
) {
9544 config
.$input
.empty();
9547 // Properties (must be set before parent constructor, which calls #setValue)
9548 this.useInputTag
= config
.useInputTag
;
9550 // Parent constructor
9551 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9553 // Mixin constructors
9554 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {
9555 $button
: this.$input
9557 OO
.ui
.mixin
.IconElement
.call( this, config
);
9558 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9559 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9560 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
9563 if ( !config
.useInputTag
) {
9564 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9566 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9571 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9572 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9573 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9574 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9575 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9576 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
9578 /* Static Properties */
9584 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9592 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9594 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9595 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9601 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9603 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9604 * text, or `null` for no label
9606 * @return {OO.ui.Widget} The widget, for chaining
9608 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9609 if ( typeof label
=== 'function' ) {
9610 label
= OO
.ui
.resolveMsg( label
);
9613 if ( this.useInputTag
) {
9614 // Discard non-plaintext labels
9615 if ( typeof label
!== 'string' ) {
9619 this.$input
.val( label
);
9622 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9626 * Set the value of the input.
9628 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9629 * they do not support {@link #value values}.
9631 * @param {string} value New value
9633 * @return {OO.ui.Widget} The widget, for chaining
9635 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9636 if ( !this.useInputTag
) {
9637 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9645 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9646 // Disable generating `<label>` elements for buttons. One would very rarely need additional
9647 // label for a button, and it's already a big clickable target, and it causes
9648 // unexpected rendering.
9653 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9654 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9655 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9656 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9658 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9661 * // An example of selected, unselected, and disabled checkbox inputs.
9662 * var checkbox1 = new OO.ui.CheckboxInputWidget( {
9666 * checkbox2 = new OO.ui.CheckboxInputWidget( {
9669 * checkbox3 = new OO.ui.CheckboxInputWidget( {
9673 * // Create a fieldset layout with fields for each checkbox.
9674 * fieldset = new OO.ui.FieldsetLayout( {
9675 * label: 'Checkboxes'
9677 * fieldset.addItems( [
9678 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9679 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9680 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9682 * $( document.body ).append( fieldset.$element );
9684 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9687 * @extends OO.ui.InputWidget
9690 * @param {Object} [config] Configuration options
9691 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is
9693 * @cfg {boolean} [indeterminate=false] Whether the checkbox is in the indeterminate state.
9695 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9696 // Configuration initialization
9697 config
= config
|| {};
9699 // Parent constructor
9700 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9703 this.checkIcon
= new OO
.ui
.IconWidget( {
9705 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9710 .addClass( 'oo-ui-checkboxInputWidget' )
9711 // Required for pretty styling in WikimediaUI theme
9712 .append( this.checkIcon
.$element
);
9713 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9714 this.setIndeterminate( config
.indeterminate
!== undefined ? config
.indeterminate
: false );
9719 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9726 * A change event is emitted when the state of the input changes.
9728 * @param {boolean} selected
9729 * @param {boolean} indeterminate
9732 /* Static Properties */
9738 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9740 /* Static Methods */
9745 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9746 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9747 state
.checked
= config
.$input
.prop( 'checked' );
9757 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9758 return $( '<input>' ).attr( 'type', 'checkbox' );
9764 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9766 if ( !this.isDisabled() ) {
9767 // Allow the stack to clear so the value will be updated
9768 setTimeout( function () {
9769 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9770 widget
.setIndeterminate( widget
.$input
.prop( 'indeterminate' ) );
9776 * Set selection state of this checkbox.
9778 * @param {boolean} state Selected state
9779 * @param {boolean} internal Used for internal calls to suppress events
9781 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9783 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
, internal ) {
9785 if ( this.selected
!== state
) {
9786 this.selected
= state
;
9787 this.$input
.prop( 'checked', this.selected
);
9789 this.setIndeterminate( false, true );
9790 this.emit( 'change', this.selected
, this.indeterminate
);
9793 // The first time that the selection state is set (probably while constructing the widget),
9794 // remember it in defaultSelected. This property can be later used to check whether
9795 // the selection state of the input has been changed since it was created.
9796 if ( this.defaultSelected
=== undefined ) {
9797 this.defaultSelected
= this.selected
;
9798 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9804 * Check if this checkbox is selected.
9806 * @return {boolean} Checkbox is selected
9808 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9809 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9810 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9811 var selected
= this.$input
.prop( 'checked' );
9812 if ( this.selected
!== selected
) {
9813 this.setSelected( selected
);
9815 return this.selected
;
9819 * Set indeterminate state of this checkbox.
9821 * @param {boolean} state Indeterminate state
9822 * @param {boolean} internal Used for internal calls to suppress events
9824 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9826 OO
.ui
.CheckboxInputWidget
.prototype.setIndeterminate = function ( state
, internal ) {
9828 if ( this.indeterminate
!== state
) {
9829 this.indeterminate
= state
;
9830 this.$input
.prop( 'indeterminate', this.indeterminate
);
9832 this.setSelected( false, true );
9833 this.emit( 'change', this.selected
, this.indeterminate
);
9840 * Check if this checkbox is selected.
9842 * @return {boolean} Checkbox is selected
9844 OO
.ui
.CheckboxInputWidget
.prototype.isIndeterminate = function () {
9845 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9846 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9847 var indeterminate
= this.$input
.prop( 'indeterminate' );
9848 if ( this.indeterminate
!== indeterminate
) {
9849 this.setIndeterminate( indeterminate
);
9851 return this.indeterminate
;
9857 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9858 if ( !this.isDisabled() ) {
9859 this.$handle
.trigger( 'click' );
9867 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9868 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9869 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9870 this.setSelected( state
.checked
);
9875 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9876 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the
9877 * value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9878 * more information about input widgets.
9880 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9881 * are no options. If no `value` configuration option is provided, the first option is selected.
9882 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9884 * This and OO.ui.RadioSelectInputWidget support similar configuration options.
9887 * // A DropdownInputWidget with three options.
9888 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9890 * { data: 'a', label: 'First' },
9891 * { data: 'b', label: 'Second', disabled: true },
9892 * { optgroup: 'Group label' },
9893 * { data: 'c', label: 'First sub-item)' }
9896 * $( document.body ).append( dropdownInput.$element );
9898 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9901 * @extends OO.ui.InputWidget
9904 * @param {Object} [config] Configuration options
9905 * @cfg {Object[]} [options=[]] Array of menu options in the format described above.
9906 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9907 * @cfg {jQuery|boolean} [$overlay] Render the menu into a separate layer. This configuration is
9908 * useful in cases where the expanded menu is larger than its containing `<div>`. The specified
9909 * overlay layer is usually on top of the containing `<div>` and has a larger area. By default,
9910 * the menu uses relative positioning. Pass 'true' to use the default overlay.
9911 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9913 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9914 // Configuration initialization
9915 config
= config
|| {};
9917 // Properties (must be done before parent constructor which calls #setDisabled)
9918 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9920 $overlay
: config
.$overlay
9924 // Set up the options before parent constructor, which uses them to validate config.value.
9925 // Use this instead of setOptions() because this.$input is not set up yet.
9926 this.setOptionsData( config
.options
|| [] );
9928 // Parent constructor
9929 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9932 this.dropdownWidget
.getMenu().connect( this, {
9933 select
: 'onMenuSelect'
9938 .addClass( 'oo-ui-dropdownInputWidget' )
9939 .append( this.dropdownWidget
.$element
);
9940 if ( OO
.ui
.isMobile() ) {
9941 this.$element
.addClass( 'oo-ui-isMobile' );
9943 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9944 this.setTitledElement( this.dropdownWidget
.$handle
);
9949 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9957 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9958 return $( '<select>' ).addClass( 'oo-ui-indicator-down' );
9962 * Handles menu select events.
9965 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9967 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9968 this.setValue( item
? item
.getData() : '' );
9974 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9976 value
= this.cleanUpValue( value
);
9977 // Only allow setting values that are actually present in the dropdown
9978 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9979 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9980 this.dropdownWidget
.getMenu().selectItem( selected
);
9981 value
= selected
? selected
.getData() : '';
9982 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9983 if ( this.optionsDirty
) {
9984 // We reached this from the constructor or from #setOptions.
9985 // We have to update the <select> element.
9986 this.updateOptionsInterface();
9994 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9995 this.dropdownWidget
.setDisabled( state
);
9996 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10001 * Set the options available for this input.
10003 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10005 * @return {OO.ui.Widget} The widget, for chaining
10007 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
10008 var value
= this.getValue();
10010 this.setOptionsData( options
);
10012 // Re-set the value to update the visible interface (DropdownWidget and <select>).
10013 // In case the previous value is no longer an available option, select the first valid one.
10014 this.setValue( value
);
10020 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10022 * This method may be called before the parent constructor, so various properties may not be
10025 * @param {Object[]} options Array of menu options (see #constructor for details).
10028 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
10029 var optionWidgets
, optIndex
, opt
, previousOptgroup
, optionWidget
, optValue
,
10032 this.optionsDirty
= true;
10034 // Go through all the supplied option configs and create either
10035 // MenuSectionOption or MenuOption widgets from each.
10036 optionWidgets
= [];
10037 for ( optIndex
= 0; optIndex
< options
.length
; optIndex
++ ) {
10038 opt
= options
[ optIndex
];
10040 if ( opt
.optgroup
!== undefined ) {
10041 // Create a <optgroup> menu item.
10042 optionWidget
= widget
.createMenuSectionOptionWidget( opt
.optgroup
);
10043 previousOptgroup
= optionWidget
;
10046 // Create a normal <option> menu item.
10047 optValue
= widget
.cleanUpValue( opt
.data
);
10048 optionWidget
= widget
.createMenuOptionWidget(
10050 opt
.label
!== undefined ? opt
.label
: optValue
10054 // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
10056 opt
.disabled
!== undefined ||
10057 previousOptgroup
instanceof OO
.ui
.MenuSectionOptionWidget
&&
10058 previousOptgroup
.isDisabled()
10060 optionWidget
.setDisabled( true );
10063 optionWidgets
.push( optionWidget
);
10066 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
10070 * Create a menu option widget.
10073 * @param {string} data Item data
10074 * @param {string} label Item label
10075 * @return {OO.ui.MenuOptionWidget} Option widget
10077 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
10078 return new OO
.ui
.MenuOptionWidget( {
10085 * Create a menu section option widget.
10088 * @param {string} label Section item label
10089 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
10091 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
10092 return new OO
.ui
.MenuSectionOptionWidget( {
10098 * Update the user-visible interface to match the internal list of options and value.
10100 * This method must only be called after the parent constructor.
10104 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
10106 $optionsContainer
= this.$input
,
10107 defaultValue
= this.defaultValue
,
10110 this.$input
.empty();
10112 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
10115 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
10116 $optionNode
= $( '<option>' )
10117 .attr( 'value', optionWidget
.getData() )
10118 .text( optionWidget
.getLabel() );
10120 // Remember original selection state. This property can be later used to check whether
10121 // the selection state of the input has been changed since it was created.
10122 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
10124 $optionsContainer
.append( $optionNode
);
10126 $optionNode
= $( '<optgroup>' )
10127 .attr( 'label', optionWidget
.getLabel() );
10128 widget
.$input
.append( $optionNode
);
10129 $optionsContainer
= $optionNode
;
10132 // Disable the option or optgroup if required.
10133 if ( optionWidget
.isDisabled() ) {
10134 $optionNode
.prop( 'disabled', true );
10138 this.optionsDirty
= false;
10144 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
10145 this.dropdownWidget
.focus();
10152 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
10153 this.dropdownWidget
.blur();
10158 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
10159 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
10160 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
10161 * please see the [OOUI documentation on MediaWiki][1].
10163 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10166 * // An example of selected, unselected, and disabled radio inputs
10167 * var radio1 = new OO.ui.RadioInputWidget( {
10171 * var radio2 = new OO.ui.RadioInputWidget( {
10174 * var radio3 = new OO.ui.RadioInputWidget( {
10178 * // Create a fieldset layout with fields for each radio button.
10179 * var fieldset = new OO.ui.FieldsetLayout( {
10180 * label: 'Radio inputs'
10182 * fieldset.addItems( [
10183 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
10184 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
10185 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
10187 * $( document.body ).append( fieldset.$element );
10189 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10192 * @extends OO.ui.InputWidget
10195 * @param {Object} [config] Configuration options
10196 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button
10199 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
10200 // Configuration initialization
10201 config
= config
|| {};
10203 // Parent constructor
10204 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
10208 .addClass( 'oo-ui-radioInputWidget' )
10209 // Required for pretty styling in WikimediaUI theme
10210 .append( $( '<span>' ) );
10211 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
10216 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
10218 /* Static Properties */
10224 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
10226 /* Static Methods */
10231 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10232 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10233 state
.checked
= config
.$input
.prop( 'checked' );
10243 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
10244 return $( '<input>' ).attr( 'type', 'radio' );
10250 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
10251 // RadioInputWidget doesn't track its state.
10255 * Set selection state of this radio button.
10257 * @param {boolean} state `true` for selected
10259 * @return {OO.ui.Widget} The widget, for chaining
10261 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
10262 // RadioInputWidget doesn't track its state.
10263 this.$input
.prop( 'checked', state
);
10264 // The first time that the selection state is set (probably while constructing the widget),
10265 // remember it in defaultSelected. This property can be later used to check whether
10266 // the selection state of the input has been changed since it was created.
10267 if ( this.defaultSelected
=== undefined ) {
10268 this.defaultSelected
= state
;
10269 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
10275 * Check if this radio button is selected.
10277 * @return {boolean} Radio is selected
10279 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
10280 return this.$input
.prop( 'checked' );
10286 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
10287 if ( !this.isDisabled() ) {
10288 this.$input
.trigger( 'click' );
10296 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10297 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10298 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
10299 this.setSelected( state
.checked
);
10304 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be
10305 * used within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with
10306 * the value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
10307 * more information about input widgets.
10309 * This and OO.ui.DropdownInputWidget support similar configuration options.
10312 * // A RadioSelectInputWidget with three options
10313 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
10315 * { data: 'a', label: 'First' },
10316 * { data: 'b', label: 'Second'},
10317 * { data: 'c', label: 'Third' }
10320 * $( document.body ).append( radioSelectInput.$element );
10322 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10325 * @extends OO.ui.InputWidget
10328 * @param {Object} [config] Configuration options
10329 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10331 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
10332 // Configuration initialization
10333 config
= config
|| {};
10335 // Properties (must be done before parent constructor which calls #setDisabled)
10336 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
10337 // Set up the options before parent constructor, which uses them to validate config.value.
10338 // Use this instead of setOptions() because this.$input is not set up yet
10339 this.setOptionsData( config
.options
|| [] );
10341 // Parent constructor
10342 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
10345 this.radioSelectWidget
.connect( this, {
10346 select
: 'onMenuSelect'
10351 .addClass( 'oo-ui-radioSelectInputWidget' )
10352 .append( this.radioSelectWidget
.$element
);
10353 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
10358 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
10360 /* Static Methods */
10365 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10366 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10367 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
10374 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10375 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10376 // Cannot reuse the `<input type=radio>` set
10377 delete config
.$input
;
10387 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
10388 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
10389 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
10390 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
10394 * Handles menu select events.
10397 * @param {OO.ui.RadioOptionWidget} item Selected menu item
10399 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
10400 this.setValue( item
.getData() );
10406 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
10408 value
= this.cleanUpValue( value
);
10409 // Only allow setting values that are actually present in the dropdown
10410 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
10411 this.radioSelectWidget
.findFirstSelectableItem();
10412 this.radioSelectWidget
.selectItem( selected
);
10413 value
= selected
? selected
.getData() : '';
10414 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10421 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10422 this.radioSelectWidget
.setDisabled( state
);
10423 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10428 * Set the options available for this input.
10430 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10432 * @return {OO.ui.Widget} The widget, for chaining
10434 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10435 var value
= this.getValue();
10437 this.setOptionsData( options
);
10439 // Re-set the value to update the visible interface (RadioSelectWidget).
10440 // In case the previous value is no longer an available option, select the first valid one.
10441 this.setValue( value
);
10447 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10449 * This method may be called before the parent constructor, so various properties may not be
10452 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10455 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10458 this.radioSelectWidget
10460 .addItems( options
.map( function ( opt
) {
10461 var optValue
= widget
.cleanUpValue( opt
.data
);
10462 return new OO
.ui
.RadioOptionWidget( {
10464 label
: opt
.label
!== undefined ? opt
.label
: optValue
10472 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10473 this.radioSelectWidget
.focus();
10480 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10481 this.radioSelectWidget
.blur();
10486 * CheckboxMultiselectInputWidget is a
10487 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10488 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10489 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10490 * more information about input widgets.
10493 * // A CheckboxMultiselectInputWidget with three options.
10494 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10496 * { data: 'a', label: 'First' },
10497 * { data: 'b', label: 'Second' },
10498 * { data: 'c', label: 'Third' }
10501 * $( document.body ).append( multiselectInput.$element );
10503 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10506 * @extends OO.ui.InputWidget
10509 * @param {Object} [config] Configuration options
10510 * @cfg {Object[]} [options=[]] Array of menu options in the format
10511 * `{ data: …, label: …, disabled: … }`
10513 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10514 // Configuration initialization
10515 config
= config
|| {};
10517 // Properties (must be done before parent constructor which calls #setDisabled)
10518 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10519 // Must be set before the #setOptionsData call below
10520 this.inputName
= config
.name
;
10521 // Set up the options before parent constructor, which uses them to validate config.value.
10522 // Use this instead of setOptions() because this.$input is not set up yet
10523 this.setOptionsData( config
.options
|| [] );
10525 // Parent constructor
10526 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10529 this.checkboxMultiselectWidget
.connect( this, {
10530 select
: 'onCheckboxesSelect'
10535 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10536 .append( this.checkboxMultiselectWidget
.$element
);
10537 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10538 this.$input
.detach();
10543 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10545 /* Static Methods */
10550 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10551 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState(
10554 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10555 .toArray().map( function ( el
) { return el
.value
; } );
10562 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10563 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10564 // Cannot reuse the `<input type=checkbox>` set
10565 delete config
.$input
;
10575 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10577 return $( '<unused>' );
10581 * Handles CheckboxMultiselectWidget select events.
10585 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10586 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10592 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10593 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10594 .toArray().map( function ( el
) { return el
.value
; } );
10595 if ( this.value
!== value
) {
10596 this.setValue( value
);
10604 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10605 value
= this.cleanUpValue( value
);
10606 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10607 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10608 if ( this.optionsDirty
) {
10609 // We reached this from the constructor or from #setOptions.
10610 // We have to update the <select> element.
10611 this.updateOptionsInterface();
10617 * Clean up incoming value.
10619 * @param {string[]} value Original value
10620 * @return {string[]} Cleaned up value
10622 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10623 var i
, singleValue
,
10625 if ( !Array
.isArray( value
) ) {
10628 for ( i
= 0; i
< value
.length
; i
++ ) {
10629 singleValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10630 .call( this, value
[ i
] );
10631 // Remove options that we don't have here
10632 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10635 cleanValue
.push( singleValue
);
10643 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10644 this.checkboxMultiselectWidget
.setDisabled( state
);
10645 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10650 * Set the options available for this input.
10652 * @param {Object[]} options Array of menu options in the format
10653 * `{ data: …, label: …, disabled: … }`
10655 * @return {OO.ui.Widget} The widget, for chaining
10657 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10658 var value
= this.getValue();
10660 this.setOptionsData( options
);
10662 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10663 // This will also get rid of any stale options that we just removed.
10664 this.setValue( value
);
10670 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10672 * This method may be called before the parent constructor, so various properties may not be
10675 * @param {Object[]} options Array of menu options in the format
10676 * `{ data: …, label: … }`
10679 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10682 this.optionsDirty
= true;
10684 this.checkboxMultiselectWidget
10686 .addItems( options
.map( function ( opt
) {
10687 var optValue
, item
, optDisabled
;
10688 optValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10689 .call( widget
, opt
.data
);
10690 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10691 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10693 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10694 disabled
: optDisabled
10696 // Set the 'name' and 'value' for form submission
10697 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10698 item
.checkbox
.setValue( optValue
);
10704 * Update the user-visible interface to match the internal list of options and value.
10706 * This method must only be called after the parent constructor.
10710 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10711 var defaultValue
= this.defaultValue
;
10713 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10714 // Remember original selection state. This property can be later used to check whether
10715 // the selection state of the input has been changed since it was created.
10716 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10717 item
.checkbox
.defaultSelected
= isDefault
;
10718 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10721 this.optionsDirty
= false;
10727 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10728 this.checkboxMultiselectWidget
.focus();
10733 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10734 * size of the field as well as its presentation. In addition, these widgets can be configured
10735 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an
10736 * optional validation-pattern (used to determine if an input value is valid or not) and an input
10737 * filter, which modifies incoming values rather than validating them.
10738 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10740 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10743 * // A TextInputWidget.
10744 * var textInput = new OO.ui.TextInputWidget( {
10745 * value: 'Text input'
10747 * $( document.body ).append( textInput.$element );
10749 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10752 * @extends OO.ui.InputWidget
10753 * @mixins OO.ui.mixin.IconElement
10754 * @mixins OO.ui.mixin.IndicatorElement
10755 * @mixins OO.ui.mixin.PendingElement
10756 * @mixins OO.ui.mixin.LabelElement
10757 * @mixins OO.ui.mixin.FlaggedElement
10760 * @param {Object} [config] Configuration options
10761 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10762 * 'email', 'url' or 'number'.
10763 * @cfg {string} [placeholder] Placeholder text
10764 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10765 * instruct the browser to focus this widget.
10766 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10767 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10769 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10770 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10771 * many emojis) count as 2 characters each.
10772 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10773 * the value or placeholder text: `'before'` or `'after'`
10774 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator:
10775 * 'required'`. Note that `false` & setting `indicator: 'required' will result in no indicator
10777 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10778 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined`
10779 * means leaving it up to the browser).
10780 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10781 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10782 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10783 * value for it to be considered valid; when Function, a function receiving the value as parameter
10784 * that must return true, or promise resolving to true, for it to be considered valid.
10786 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10787 // Configuration initialization
10788 config
= $.extend( {
10790 labelPosition
: 'after'
10793 // Parent constructor
10794 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10796 // Mixin constructors
10797 OO
.ui
.mixin
.IconElement
.call( this, config
);
10798 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10799 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( { $pending
: this.$input
}, config
) );
10800 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10801 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
10804 this.type
= this.getSaneType( config
);
10805 this.readOnly
= false;
10806 this.required
= false;
10807 this.validate
= null;
10808 this.scrollWidth
= null;
10810 this.setValidation( config
.validate
);
10811 this.setLabelPosition( config
.labelPosition
);
10815 keypress
: this.onKeyPress
.bind( this ),
10816 blur
: this.onBlur
.bind( this ),
10817 focus
: this.onFocus
.bind( this )
10819 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10820 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10821 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10822 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10826 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10827 .append( this.$icon
, this.$indicator
);
10828 this.setReadOnly( !!config
.readOnly
);
10829 this.setRequired( !!config
.required
);
10830 if ( config
.placeholder
!== undefined ) {
10831 this.$input
.attr( 'placeholder', config
.placeholder
);
10833 if ( config
.maxLength
!== undefined ) {
10834 this.$input
.attr( 'maxlength', config
.maxLength
);
10836 if ( config
.autofocus
) {
10837 this.$input
.attr( 'autofocus', 'autofocus' );
10839 if ( config
.autocomplete
=== false ) {
10840 this.$input
.attr( 'autocomplete', 'off' );
10841 // Turning off autocompletion also disables "form caching" when the user navigates to a
10842 // different page and then clicks "Back". Re-enable it when leaving.
10843 // Borrowed from jQuery UI.
10845 beforeunload: function () {
10846 this.$input
.removeAttr( 'autocomplete' );
10848 pageshow: function () {
10849 // Browsers don't seem to actually fire this event on "Back", they instead just
10850 // reload the whole page... it shouldn't hurt, though.
10851 this.$input
.attr( 'autocomplete', 'off' );
10855 if ( config
.spellcheck
!== undefined ) {
10856 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10858 if ( this.label
) {
10859 this.isWaitingToBeAttached
= true;
10860 this.installParentChangeDetector();
10866 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10867 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10868 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10869 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10870 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10871 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
10873 /* Static Properties */
10875 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10883 * An `enter` event is emitted when the user presses Enter key inside the text box.
10891 * Handle icon mouse down events.
10894 * @param {jQuery.Event} e Mouse down event
10895 * @return {undefined|boolean} False to prevent default if event is handled
10897 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10898 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10905 * Handle indicator mouse down events.
10908 * @param {jQuery.Event} e Mouse down event
10909 * @return {undefined|boolean} False to prevent default if event is handled
10911 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10912 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10919 * Handle key press events.
10922 * @param {jQuery.Event} e Key press event
10923 * @fires enter If Enter key is pressed
10925 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10926 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10927 this.emit( 'enter', e
);
10932 * Handle blur events.
10935 * @param {jQuery.Event} e Blur event
10937 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10938 this.setValidityFlag();
10942 * Handle focus events.
10945 * @param {jQuery.Event} e Focus event
10947 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10948 if ( this.isWaitingToBeAttached
) {
10949 // If we've received focus, then we must be attached to the document, and if
10950 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10951 this.onElementAttach();
10953 this.setValidityFlag( true );
10957 * Handle element attach events.
10960 * @param {jQuery.Event} e Element attach event
10962 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10963 this.isWaitingToBeAttached
= false;
10964 // Any previously calculated size is now probably invalid if we reattached elsewhere
10965 this.valCache
= null;
10966 this.positionLabel();
10970 * Handle debounced change events.
10972 * @param {string} value
10975 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10976 this.setValidityFlag();
10980 * Check if the input is {@link #readOnly read-only}.
10982 * @return {boolean}
10984 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10985 return this.readOnly
;
10989 * Set the {@link #readOnly read-only} state of the input.
10991 * @param {boolean} state Make input read-only
10993 * @return {OO.ui.Widget} The widget, for chaining
10995 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10996 this.readOnly
= !!state
;
10997 this.$input
.prop( 'readOnly', this.readOnly
);
11002 * Check if the input is {@link #required required}.
11004 * @return {boolean}
11006 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
11007 return this.required
;
11011 * Set the {@link #required required} state of the input.
11013 * @param {boolean} state Make input required
11015 * @return {OO.ui.Widget} The widget, for chaining
11017 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
11018 this.required
= !!state
;
11019 if ( this.required
) {
11021 .prop( 'required', true )
11022 .attr( 'aria-required', 'true' );
11023 if ( this.getIndicator() === null ) {
11024 this.setIndicator( 'required' );
11028 .prop( 'required', false )
11029 .removeAttr( 'aria-required' );
11030 if ( this.getIndicator() === 'required' ) {
11031 this.setIndicator( null );
11038 * Support function for making #onElementAttach work across browsers.
11040 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
11041 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
11043 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
11044 * first time that the element gets attached to the documented.
11046 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
11047 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
11048 MutationObserver
= window
.MutationObserver
||
11049 window
.WebKitMutationObserver
||
11050 window
.MozMutationObserver
,
11053 if ( MutationObserver
) {
11054 // The new way. If only it wasn't so ugly.
11056 if ( this.isElementAttached() ) {
11057 // Widget is attached already, do nothing. This breaks the functionality of this
11058 // function when the widget is detached and reattached. Alas, doing this correctly with
11059 // MutationObserver would require observation of the whole document, which would hurt
11060 // performance of other, more important code.
11064 // Find topmost node in the tree
11065 topmostNode
= this.$element
[ 0 ];
11066 while ( topmostNode
.parentNode
) {
11067 topmostNode
= topmostNode
.parentNode
;
11070 // We have no way to detect the $element being attached somewhere without observing the
11071 // entire DOM with subtree modifications, which would hurt performance. So we cheat: we hook
11072 // to the parent node of $element, and instead detect when $element is removed from it (and
11073 // thus probably attached somewhere else). If there is no parent, we create a "fake" one. If
11074 // it doesn't get attached, we end up back here and create the parent.
11075 mutationObserver
= new MutationObserver( function ( mutations
) {
11076 var i
, j
, removedNodes
;
11077 for ( i
= 0; i
< mutations
.length
; i
++ ) {
11078 removedNodes
= mutations
[ i
].removedNodes
;
11079 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
11080 if ( removedNodes
[ j
] === topmostNode
) {
11081 setTimeout( onRemove
, 0 );
11088 onRemove = function () {
11089 // If the node was attached somewhere else, report it
11090 if ( widget
.isElementAttached() ) {
11091 widget
.onElementAttach();
11093 mutationObserver
.disconnect();
11094 widget
.installParentChangeDetector();
11097 // Create a fake parent and observe it
11098 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
11099 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
11101 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
11102 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
11103 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
11111 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
11112 if ( this.getSaneType( config
) === 'number' ) {
11113 return $( '<input>' )
11114 .attr( 'step', 'any' )
11115 .attr( 'type', 'number' );
11117 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
11122 * Get sanitized value for 'type' for given config.
11124 * @param {Object} config Configuration options
11125 * @return {string|null}
11128 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
11129 var allowedTypes
= [
11136 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
11140 * Focus the input and select a specified range within the text.
11142 * @param {number} from Select from offset
11143 * @param {number} [to] Select to offset, defaults to from
11145 * @return {OO.ui.Widget} The widget, for chaining
11147 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
11148 var isBackwards
, start
, end
,
11149 input
= this.$input
[ 0 ];
11153 isBackwards
= to
< from;
11154 start
= isBackwards
? to
: from;
11155 end
= isBackwards
? from : to
;
11160 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
11162 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
11163 // Rather than expensively check if the input is attached every time, just check
11164 // if it was the cause of an error being thrown. If not, rethrow the error.
11165 if ( this.getElementDocument().body
.contains( input
) ) {
11173 * Get an object describing the current selection range in a directional manner
11175 * @return {Object} Object containing 'from' and 'to' offsets
11177 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
11178 var input
= this.$input
[ 0 ],
11179 start
= input
.selectionStart
,
11180 end
= input
.selectionEnd
,
11181 isBackwards
= input
.selectionDirection
=== 'backward';
11184 from: isBackwards
? end
: start
,
11185 to
: isBackwards
? start
: end
11190 * Get the length of the text input value.
11192 * This could differ from the length of #getValue if the
11193 * value gets filtered
11195 * @return {number} Input length
11197 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
11198 return this.$input
[ 0 ].value
.length
;
11202 * Focus the input and select the entire text.
11205 * @return {OO.ui.Widget} The widget, for chaining
11207 OO
.ui
.TextInputWidget
.prototype.select = function () {
11208 return this.selectRange( 0, this.getInputLength() );
11212 * Focus the input and move the cursor to the start.
11215 * @return {OO.ui.Widget} The widget, for chaining
11217 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
11218 return this.selectRange( 0 );
11222 * Focus the input and move the cursor to the end.
11225 * @return {OO.ui.Widget} The widget, for chaining
11227 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
11228 return this.selectRange( this.getInputLength() );
11232 * Insert new content into the input.
11234 * @param {string} content Content to be inserted
11236 * @return {OO.ui.Widget} The widget, for chaining
11238 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
11240 range
= this.getRange(),
11241 value
= this.getValue();
11243 start
= Math
.min( range
.from, range
.to
);
11244 end
= Math
.max( range
.from, range
.to
);
11246 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
11247 this.selectRange( start
+ content
.length
);
11252 * Insert new content either side of a selection.
11254 * @param {string} pre Content to be inserted before the selection
11255 * @param {string} post Content to be inserted after the selection
11257 * @return {OO.ui.Widget} The widget, for chaining
11259 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
11261 range
= this.getRange(),
11262 offset
= pre
.length
;
11264 start
= Math
.min( range
.from, range
.to
);
11265 end
= Math
.max( range
.from, range
.to
);
11267 this.selectRange( start
).insertContent( pre
);
11268 this.selectRange( offset
+ end
).insertContent( post
);
11270 this.selectRange( offset
+ start
, offset
+ end
);
11275 * Set the validation pattern.
11277 * The validation pattern is either a regular expression, a function, or the symbolic name of a
11278 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11279 * value must contain only numbers).
11281 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
11282 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11284 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
11285 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
11286 this.validate
= validate
;
11288 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
11293 * Sets the 'invalid' flag appropriately.
11295 * @param {boolean} [isValid] Optionally override validation result
11297 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
11299 setFlag = function ( valid
) {
11301 widget
.$input
.attr( 'aria-invalid', 'true' );
11303 widget
.$input
.removeAttr( 'aria-invalid' );
11305 widget
.setFlags( { invalid
: !valid
} );
11308 if ( isValid
!== undefined ) {
11309 setFlag( isValid
);
11311 this.getValidity().then( function () {
11320 * Get the validity of current value.
11322 * This method returns a promise that resolves if the value is valid and rejects if
11323 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
11325 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
11327 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
11330 function rejectOrResolve( valid
) {
11332 return $.Deferred().resolve().promise();
11334 return $.Deferred().reject().promise();
11338 // Check browser validity and reject if it is invalid
11340 this.$input
[ 0 ].checkValidity
!== undefined &&
11341 this.$input
[ 0 ].checkValidity() === false
11343 return rejectOrResolve( false );
11346 // Run our checks if the browser thinks the field is valid
11347 if ( this.validate
instanceof Function
) {
11348 result
= this.validate( this.getValue() );
11349 if ( result
&& typeof result
.promise
=== 'function' ) {
11350 return result
.promise().then( function ( valid
) {
11351 return rejectOrResolve( valid
);
11354 return rejectOrResolve( result
);
11357 return rejectOrResolve( this.getValue().match( this.validate
) );
11362 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11364 * @param {string} labelPosition Label position, 'before' or 'after'
11366 * @return {OO.ui.Widget} The widget, for chaining
11368 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
11369 this.labelPosition
= labelPosition
;
11370 if ( this.label
) {
11371 // If there is no label and we only change the position, #updatePosition is a no-op,
11372 // but it takes really a lot of work to do nothing.
11373 this.updatePosition();
11379 * Update the position of the inline label.
11381 * This method is called by #setLabelPosition, and can also be called on its own if
11382 * something causes the label to be mispositioned.
11385 * @return {OO.ui.Widget} The widget, for chaining
11387 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
11388 var after
= this.labelPosition
=== 'after';
11391 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
11392 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
11394 this.valCache
= null;
11395 this.scrollWidth
= null;
11396 this.positionLabel();
11402 * Position the label by setting the correct padding on the input.
11406 * @return {OO.ui.Widget} The widget, for chaining
11408 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11409 var after
, rtl
, property
, newCss
;
11411 if ( this.isWaitingToBeAttached
) {
11412 // #onElementAttach will be called soon, which calls this method
11417 'padding-right': '',
11421 if ( this.label
) {
11422 this.$element
.append( this.$label
);
11424 this.$label
.detach();
11425 // Clear old values if present
11426 this.$input
.css( newCss
);
11430 after
= this.labelPosition
=== 'after';
11431 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11432 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11434 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11435 // We have to clear the padding on the other side, in case the element direction changed
11436 this.$input
.css( newCss
);
11442 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11443 * {@link OO.ui.mixin.IconElement search icon} by default.
11444 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11446 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11449 * @extends OO.ui.TextInputWidget
11452 * @param {Object} [config] Configuration options
11454 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11455 config
= $.extend( {
11459 // Parent constructor
11460 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11463 this.connect( this, {
11466 this.$indicator
.on( 'click', this.onIndicatorClick
.bind( this ) );
11469 this.updateSearchIndicator();
11470 this.connect( this, {
11471 disable
: 'onDisable'
11477 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11485 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11490 * Handle click events on the indicator
11492 * @param {jQuery.Event} e Click event
11493 * @return {boolean}
11495 OO
.ui
.SearchInputWidget
.prototype.onIndicatorClick = function ( e
) {
11496 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11497 // Clear the text field
11498 this.setValue( '' );
11505 * Update the 'clear' indicator displayed on type: 'search' text
11506 * fields, hiding it when the field is already empty or when it's not
11509 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11510 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11511 this.setIndicator( null );
11513 this.setIndicator( 'clear' );
11518 * Handle change events.
11522 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11523 this.updateSearchIndicator();
11527 * Handle disable events.
11529 * @param {boolean} disabled Element is disabled
11532 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11533 this.updateSearchIndicator();
11539 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11540 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11541 this.updateSearchIndicator();
11546 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11547 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11548 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11549 * {@link OO.ui.mixin.IndicatorElement indicators}.
11550 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11552 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11555 * // A MultilineTextInputWidget.
11556 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11557 * value: 'Text input on multiple lines'
11559 * $( document.body ).append( multilineTextInput.$element );
11561 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11564 * @extends OO.ui.TextInputWidget
11567 * @param {Object} [config] Configuration options
11568 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11569 * specifies minimum number of rows to display.
11570 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11571 * Use the #maxRows config to specify a maximum number of displayed rows.
11572 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11573 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11575 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11576 config
= $.extend( {
11579 // Parent constructor
11580 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11583 this.autosize
= !!config
.autosize
;
11584 this.styleHeight
= null;
11585 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11586 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11588 // Clone for resizing
11589 if ( this.autosize
) {
11590 this.$clone
= this.$input
11592 .removeAttr( 'id' )
11593 .removeAttr( 'name' )
11594 .insertAfter( this.$input
)
11595 .attr( 'aria-hidden', 'true' )
11596 .addClass( 'oo-ui-element-hidden' );
11600 this.connect( this, {
11605 if ( config
.rows
) {
11606 this.$input
.attr( 'rows', config
.rows
);
11608 if ( this.autosize
) {
11609 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11610 this.isWaitingToBeAttached
= true;
11611 this.installParentChangeDetector();
11617 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11619 /* Static Methods */
11624 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11625 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11626 state
.scrollTop
= config
.$input
.scrollTop();
11635 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11636 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11641 * Handle change events.
11645 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11652 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11653 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11660 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11662 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11664 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11665 // Some platforms emit keycode 10 for Control+Enter keypress in a textarea
11668 this.emit( 'enter', e
);
11673 * Automatically adjust the size of the text input.
11675 * This only affects multiline inputs that are {@link #autosize autosized}.
11678 * @return {OO.ui.Widget} The widget, for chaining
11681 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11682 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11683 idealHeight
, newHeight
, scrollWidth
, property
;
11685 if ( this.$input
.val() !== this.valCache
) {
11686 if ( this.autosize
) {
11688 .val( this.$input
.val() )
11689 .attr( 'rows', this.minRows
)
11690 // Set inline height property to 0 to measure scroll height
11691 .css( 'height', 0 );
11693 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11695 this.valCache
= this.$input
.val();
11697 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11699 // Remove inline height property to measure natural heights
11700 this.$clone
.css( 'height', '' );
11701 innerHeight
= this.$clone
.innerHeight();
11702 outerHeight
= this.$clone
.outerHeight();
11704 // Measure max rows height
11706 .attr( 'rows', this.maxRows
)
11707 .css( 'height', 'auto' )
11709 maxInnerHeight
= this.$clone
.innerHeight();
11711 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11712 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11713 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11714 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11716 this.$clone
.addClass( 'oo-ui-element-hidden' );
11718 // Only apply inline height when expansion beyond natural height is needed
11719 // Use the difference between the inner and outer height as a buffer
11720 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11721 if ( newHeight
!== this.styleHeight
) {
11722 this.$input
.css( 'height', newHeight
);
11723 this.styleHeight
= newHeight
;
11724 this.emit( 'resize' );
11727 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11728 if ( scrollWidth
!== this.scrollWidth
) {
11729 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11731 this.$label
.css( { right
: '', left
: '' } );
11732 this.$indicator
.css( { right
: '', left
: '' } );
11734 if ( scrollWidth
) {
11735 this.$indicator
.css( property
, scrollWidth
);
11736 if ( this.labelPosition
=== 'after' ) {
11737 this.$label
.css( property
, scrollWidth
);
11741 this.scrollWidth
= scrollWidth
;
11742 this.positionLabel();
11752 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11753 return $( '<textarea>' );
11757 * Check if the input automatically adjusts its size.
11759 * @return {boolean}
11761 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11762 return !!this.autosize
;
11768 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11769 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11770 if ( state
.scrollTop
!== undefined ) {
11771 this.$input
.scrollTop( state
.scrollTop
);
11776 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11777 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11778 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11780 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11781 * option, that option will appear to be selected.
11782 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11785 * After the user chooses an option, its `data` will be used as a new value for the widget.
11786 * A `label` also can be specified for each option: if given, it will be shown instead of the
11787 * `data` in the dropdown menu.
11789 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11791 * For more information about menus and options, please see the
11792 * [OOUI documentation on MediaWiki][1].
11795 * // A ComboBoxInputWidget.
11796 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11797 * value: 'Option 1',
11799 * { data: 'Option 1' },
11800 * { data: 'Option 2' },
11801 * { data: 'Option 3' }
11804 * $( document.body ).append( comboBox.$element );
11807 * // Example: A ComboBoxInputWidget with additional option labels.
11808 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11809 * value: 'Option 1',
11812 * data: 'Option 1',
11813 * label: 'Option One'
11816 * data: 'Option 2',
11817 * label: 'Option Two'
11820 * data: 'Option 3',
11821 * label: 'Option Three'
11825 * $( document.body ).append( comboBox.$element );
11827 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11830 * @extends OO.ui.TextInputWidget
11833 * @param {Object} [config] Configuration options
11834 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11835 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu
11837 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
11838 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
11839 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
11840 * uses relative positioning.
11841 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11843 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11844 // Configuration initialization
11845 config
= $.extend( {
11846 autocomplete
: false
11849 // ComboBoxInputWidget shouldn't support `multiline`
11850 config
.multiline
= false;
11852 // See InputWidget#reusePreInfuseDOM about `config.$input`
11853 if ( config
.$input
) {
11854 config
.$input
.removeAttr( 'list' );
11857 // Parent constructor
11858 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11861 this.$overlay
= ( config
.$overlay
=== true ?
11862 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11863 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11864 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11865 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11867 invisibleLabel
: true,
11868 disabled
: this.disabled
11870 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11874 $floatableContainer
: this.$element
,
11875 disabled
: this.isDisabled()
11881 this.connect( this, {
11882 change
: 'onInputChange',
11883 enter
: 'onInputEnter'
11885 this.dropdownButton
.connect( this, {
11886 click
: 'onDropdownButtonClick'
11888 this.menu
.connect( this, {
11889 choose
: 'onMenuChoose',
11890 add
: 'onMenuItemsChange',
11891 remove
: 'onMenuItemsChange',
11892 toggle
: 'onMenuToggle'
11896 this.$input
.attr( {
11898 'aria-owns': this.menu
.getElementId(),
11899 'aria-autocomplete': 'list'
11901 this.dropdownButton
.$button
.attr( {
11902 'aria-controls': this.menu
.getElementId()
11904 // Do not override options set via config.menu.items
11905 if ( config
.options
!== undefined ) {
11906 this.setOptions( config
.options
);
11908 this.$field
= $( '<div>' )
11909 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11910 .append( this.$input
, this.dropdownButton
.$element
);
11912 .addClass( 'oo-ui-comboBoxInputWidget' )
11913 .append( this.$field
);
11914 this.$overlay
.append( this.menu
.$element
);
11915 this.onMenuItemsChange();
11920 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11925 * Get the combobox's menu.
11927 * @return {OO.ui.MenuSelectWidget} Menu widget
11929 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11934 * Get the combobox's text input widget.
11936 * @return {OO.ui.TextInputWidget} Text input widget
11938 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11943 * Handle input change events.
11946 * @param {string} value New value
11948 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11949 var match
= this.menu
.findItemFromData( value
);
11951 this.menu
.selectItem( match
);
11952 if ( this.menu
.findHighlightedItem() ) {
11953 this.menu
.highlightItem( match
);
11956 if ( !this.isDisabled() ) {
11957 this.menu
.toggle( true );
11962 * Handle input enter events.
11966 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11967 if ( !this.isDisabled() ) {
11968 this.menu
.toggle( false );
11973 * Handle button click events.
11977 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11978 this.menu
.toggle();
11983 * Handle menu choose events.
11986 * @param {OO.ui.OptionWidget} item Chosen item
11988 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11989 this.setValue( item
.getData() );
11993 * Handle menu item change events.
11997 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11998 var match
= this.menu
.findItemFromData( this.getValue() );
11999 this.menu
.selectItem( match
);
12000 if ( this.menu
.findHighlightedItem() ) {
12001 this.menu
.highlightItem( match
);
12003 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
12007 * Handle menu toggle events.
12010 * @param {boolean} isVisible Open state of the menu
12012 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
12013 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
12017 * Update the disabled state of the controls
12021 * @return {OO.ui.ComboBoxInputWidget} The widget, for chaining
12023 OO
.ui
.ComboBoxInputWidget
.prototype.updateControlsDisabled = function () {
12024 var disabled
= this.isDisabled() || this.isReadOnly();
12025 if ( this.dropdownButton
) {
12026 this.dropdownButton
.setDisabled( disabled
);
12029 this.menu
.setDisabled( disabled
);
12037 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function () {
12039 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.apply( this, arguments
);
12040 this.updateControlsDisabled();
12047 OO
.ui
.ComboBoxInputWidget
.prototype.setReadOnly = function () {
12049 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
12050 this.updateControlsDisabled();
12055 * Set the options available for this input.
12057 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
12059 * @return {OO.ui.Widget} The widget, for chaining
12061 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
12064 .addItems( options
.map( function ( opt
) {
12065 return new OO
.ui
.MenuOptionWidget( {
12067 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
12075 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
12076 * which is a widget that is specified by reference before any optional configuration settings.
12078 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of
12081 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12082 * A left-alignment is used for forms with many fields.
12083 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12084 * A right-alignment is used for long but familiar forms which users tab through,
12085 * verifying the current field with a quick glance at the label.
12086 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12087 * that users fill out from top to bottom.
12088 * - **inline**: The label is placed after the field-widget and aligned to the left.
12089 * An inline-alignment is best used with checkboxes or radio buttons.
12091 * Help text can either be:
12093 * - accessed via a help icon that appears in the upper right corner of the rendered field layout,
12095 * - shown as a subtle explanation below the label.
12097 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`.
12098 * If it is long or not essential, leave `helpInline` to its default, `false`.
12100 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
12102 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12105 * @extends OO.ui.Layout
12106 * @mixins OO.ui.mixin.LabelElement
12107 * @mixins OO.ui.mixin.TitledElement
12110 * @param {OO.ui.Widget} fieldWidget Field widget
12111 * @param {Object} [config] Configuration options
12112 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
12114 * @cfg {Array} [errors] Error messages about the widget, which will be
12115 * displayed below the widget.
12116 * @cfg {Array} [warnings] Warning messages about the widget, which will be
12117 * displayed below the widget.
12118 * @cfg {Array} [successMessages] Success messages on user interactions with the widget,
12119 * which will be displayed below the widget.
12120 * The array may contain strings or OO.ui.HtmlSnippet instances.
12121 * @cfg {Array} [notices] Notices about the widget, which will be displayed
12122 * below the widget.
12123 * The array may contain strings or OO.ui.HtmlSnippet instances.
12124 * These are more visible than `help` messages when `helpInline` is set, and so
12125 * might be good for transient messages.
12126 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
12127 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
12128 * corner of the rendered field; clicking it will display the text in a popup.
12129 * If `helpInline` is `true`, then a subtle description will be shown after the
12131 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
12132 * or shown when the "help" icon is clicked.
12133 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
12135 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12137 * @throws {Error} An error is thrown if no widget is specified
12139 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
12140 // Allow passing positional parameters inside the config object
12141 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12142 config
= fieldWidget
;
12143 fieldWidget
= config
.fieldWidget
;
12146 // Make sure we have required constructor arguments
12147 if ( fieldWidget
=== undefined ) {
12148 throw new Error( 'Widget not found' );
12151 // Configuration initialization
12152 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
12154 // Parent constructor
12155 OO
.ui
.FieldLayout
.parent
.call( this, config
);
12157 // Mixin constructors
12158 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
12159 $label
: $( '<label>' )
12161 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( { $titled
: this.$label
}, config
) );
12164 this.fieldWidget
= fieldWidget
;
12166 this.warnings
= [];
12167 this.successMessages
= [];
12169 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12170 this.$messages
= $( '<div>' );
12171 this.$header
= $( '<span>' );
12172 this.$body
= $( '<div>' );
12174 this.helpInline
= config
.helpInline
;
12177 this.fieldWidget
.connect( this, {
12178 disable
: 'onFieldDisable'
12182 this.$help
= config
.help
?
12183 this.createHelpElement( config
.help
, config
.$overlay
) :
12185 if ( this.fieldWidget
.getInputId() ) {
12186 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
12187 if ( this.helpInline
) {
12188 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
12191 this.$label
.on( 'click', function () {
12192 this.fieldWidget
.simulateLabelClick();
12194 if ( this.helpInline
) {
12195 this.$help
.on( 'click', function () {
12196 this.fieldWidget
.simulateLabelClick();
12201 .addClass( 'oo-ui-fieldLayout' )
12202 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
12203 .append( this.$body
);
12204 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
12205 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
12206 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
12208 .addClass( 'oo-ui-fieldLayout-field' )
12209 .append( this.fieldWidget
.$element
);
12211 this.setErrors( config
.errors
|| [] );
12212 this.setWarnings( config
.warnings
|| [] );
12213 this.setSuccess( config
.successMessages
|| [] );
12214 this.setNotices( config
.notices
|| [] );
12215 this.setAlignment( config
.align
);
12216 // Call this again to take into account the widget's accessKey
12217 this.updateTitle();
12222 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
12223 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
12224 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
12229 * Handle field disable events.
12232 * @param {boolean} value Field is disabled
12234 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
12235 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
12239 * Get the widget contained by the field.
12241 * @return {OO.ui.Widget} Field widget
12243 OO
.ui
.FieldLayout
.prototype.getField = function () {
12244 return this.fieldWidget
;
12248 * Return `true` if the given field widget can be used with `'inline'` alignment (see
12249 * #setAlignment). Return `false` if it can't or if this can't be determined.
12251 * @return {boolean}
12253 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
12254 // This is very simplistic, but should be good enough.
12255 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
12260 * @param {string} kind 'error' or 'notice'
12261 * @param {string|OO.ui.HtmlSnippet} text
12264 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
12265 return new OO
.ui
.MessageWidget( {
12273 * Set the field alignment mode.
12276 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
12278 * @return {OO.ui.BookletLayout} The layout, for chaining
12280 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
12281 if ( value
!== this.align
) {
12282 // Default to 'left'
12283 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
12287 if ( value
=== 'inline' && !this.isFieldInline() ) {
12290 // Reorder elements
12292 if ( this.helpInline
) {
12293 if ( value
=== 'top' ) {
12294 this.$header
.append( this.$label
);
12295 this.$body
.append( this.$header
, this.$field
, this.$help
);
12296 } else if ( value
=== 'inline' ) {
12297 this.$header
.append( this.$label
, this.$help
);
12298 this.$body
.append( this.$field
, this.$header
);
12300 this.$header
.append( this.$label
, this.$help
);
12301 this.$body
.append( this.$header
, this.$field
);
12304 if ( value
=== 'top' ) {
12305 this.$header
.append( this.$help
, this.$label
);
12306 this.$body
.append( this.$header
, this.$field
);
12307 } else if ( value
=== 'inline' ) {
12308 this.$header
.append( this.$help
, this.$label
);
12309 this.$body
.append( this.$field
, this.$header
);
12311 this.$header
.append( this.$label
);
12312 this.$body
.append( this.$header
, this.$help
, this.$field
);
12315 // Set classes. The following classes can be used here:
12316 // * oo-ui-fieldLayout-align-left
12317 // * oo-ui-fieldLayout-align-right
12318 // * oo-ui-fieldLayout-align-top
12319 // * oo-ui-fieldLayout-align-inline
12320 if ( this.align
) {
12321 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
12323 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
12324 this.align
= value
;
12331 * Set the list of error messages.
12333 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
12334 * The array may contain strings or OO.ui.HtmlSnippet instances.
12336 * @return {OO.ui.BookletLayout} The layout, for chaining
12338 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
12339 this.errors
= errors
.slice();
12340 this.updateMessages();
12345 * Set the list of warning messages.
12347 * @param {Array} warnings Warning messages about the widget, which will be displayed below
12349 * The array may contain strings or OO.ui.HtmlSnippet instances.
12351 * @return {OO.ui.BookletLayout} The layout, for chaining
12353 OO
.ui
.FieldLayout
.prototype.setWarnings = function ( warnings
) {
12354 this.warnings
= warnings
.slice();
12355 this.updateMessages();
12360 * Set the list of success messages.
12362 * @param {Array} successMessages Success messages about the widget, which will be displayed below
12364 * The array may contain strings or OO.ui.HtmlSnippet instances.
12366 * @return {OO.ui.BookletLayout} The layout, for chaining
12368 OO
.ui
.FieldLayout
.prototype.setSuccess = function ( successMessages
) {
12369 this.successMessages
= successMessages
.slice();
12370 this.updateMessages();
12375 * Set the list of notice messages.
12377 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
12378 * The array may contain strings or OO.ui.HtmlSnippet instances.
12380 * @return {OO.ui.BookletLayout} The layout, for chaining
12382 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
12383 this.notices
= notices
.slice();
12384 this.updateMessages();
12389 * Update the rendering of error, warning, success and notice messages.
12393 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
12395 this.$messages
.empty();
12398 this.errors
.length
||
12399 this.warnings
.length
||
12400 this.successMessages
.length
||
12401 this.notices
.length
12403 this.$body
.after( this.$messages
);
12405 this.$messages
.remove();
12409 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
12410 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
12412 for ( i
= 0; i
< this.warnings
.length
; i
++ ) {
12413 this.$messages
.append( this.makeMessage( 'warning', this.warnings
[ i
] ) );
12415 for ( i
= 0; i
< this.successMessages
.length
; i
++ ) {
12416 this.$messages
.append( this.makeMessage( 'success', this.successMessages
[ i
] ) );
12418 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
12419 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
12424 * Include information about the widget's accessKey in our title. TitledElement calls this method.
12425 * (This is a bit of a hack.)
12428 * @param {string} title Tooltip label for 'title' attribute
12431 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
12432 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
12433 return this.fieldWidget
.formatTitleWithAccessKey( title
);
12439 * Creates and returns the help element. Also sets the `aria-describedby`
12440 * attribute on the main element of the `fieldWidget`.
12443 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
12444 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
12445 * @return {jQuery} The element that should become `this.$help`.
12447 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
12448 var helpId
, helpWidget
;
12450 if ( this.helpInline
) {
12451 helpWidget
= new OO
.ui
.LabelWidget( {
12453 classes
: [ 'oo-ui-inline-help' ]
12456 helpId
= helpWidget
.getElementId();
12458 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12459 $overlay
: $overlay
,
12463 classes
: [ 'oo-ui-fieldLayout-help' ],
12466 label
: OO
.ui
.msg( 'ooui-field-help' ),
12467 invisibleLabel
: true
12469 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
12470 helpWidget
.getPopup().$body
.html( help
.toString() );
12472 helpWidget
.getPopup().$body
.text( help
);
12475 helpId
= helpWidget
.getPopup().getBodyId();
12478 // Set the 'aria-describedby' attribute on the fieldWidget
12479 // Preference given to an input or a button
12481 this.fieldWidget
.$input
||
12482 this.fieldWidget
.$button
||
12483 this.fieldWidget
.$element
12484 ).attr( 'aria-describedby', helpId
);
12486 return helpWidget
.$element
;
12490 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget,
12491 * a button, and an optional label and/or help text. The field-widget (e.g., a
12492 * {@link OO.ui.TextInputWidget TextInputWidget}), is required and is specified before any optional
12493 * configuration settings.
12495 * Labels can be aligned in one of four ways:
12497 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12498 * A left-alignment is used for forms with many fields.
12499 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12500 * A right-alignment is used for long but familiar forms which users tab through,
12501 * verifying the current field with a quick glance at the label.
12502 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12503 * that users fill out from top to bottom.
12504 * - **inline**: The label is placed after the field-widget and aligned to the left.
12505 * An inline-alignment is best used with checkboxes or radio buttons.
12507 * Help text is accessed via a help icon that appears in the upper right corner of the rendered
12508 * field layout when help text is specified.
12511 * // Example of an ActionFieldLayout
12512 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12513 * new OO.ui.TextInputWidget( {
12514 * placeholder: 'Field widget'
12516 * new OO.ui.ButtonWidget( {
12520 * label: 'An ActionFieldLayout. This label is aligned top',
12522 * help: 'This is help text'
12526 * $( document.body ).append( actionFieldLayout.$element );
12529 * @extends OO.ui.FieldLayout
12532 * @param {OO.ui.Widget} fieldWidget Field widget
12533 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12534 * @param {Object} config
12536 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12537 // Allow passing positional parameters inside the config object
12538 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12539 config
= fieldWidget
;
12540 fieldWidget
= config
.fieldWidget
;
12541 buttonWidget
= config
.buttonWidget
;
12544 // Parent constructor
12545 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12548 this.buttonWidget
= buttonWidget
;
12549 this.$button
= $( '<span>' );
12550 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12553 this.$element
.addClass( 'oo-ui-actionFieldLayout' );
12555 .addClass( 'oo-ui-actionFieldLayout-button' )
12556 .append( this.buttonWidget
.$element
);
12558 .addClass( 'oo-ui-actionFieldLayout-input' )
12559 .append( this.fieldWidget
.$element
);
12560 this.$field
.append( this.$input
, this.$button
);
12565 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12568 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12569 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12570 * configured with a label as well. For more information and examples,
12571 * please see the [OOUI documentation on MediaWiki][1].
12574 * // Example of a fieldset layout
12575 * var input1 = new OO.ui.TextInputWidget( {
12576 * placeholder: 'A text input field'
12579 * var input2 = new OO.ui.TextInputWidget( {
12580 * placeholder: 'A text input field'
12583 * var fieldset = new OO.ui.FieldsetLayout( {
12584 * label: 'Example of a fieldset layout'
12587 * fieldset.addItems( [
12588 * new OO.ui.FieldLayout( input1, {
12589 * label: 'Field One'
12591 * new OO.ui.FieldLayout( input2, {
12592 * label: 'Field Two'
12595 * $( document.body ).append( fieldset.$element );
12597 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12600 * @extends OO.ui.Layout
12601 * @mixins OO.ui.mixin.IconElement
12602 * @mixins OO.ui.mixin.LabelElement
12603 * @mixins OO.ui.mixin.GroupElement
12606 * @param {Object} [config] Configuration options
12607 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset.
12608 * See OO.ui.FieldLayout for more information about fields.
12609 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
12610 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
12611 * corner of the rendered field; clicking it will display the text in a popup.
12612 * If `helpInline` is `true`, then a subtle description will be shown after the
12614 * For feedback messages, you are advised to use `notices`.
12615 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
12616 * or shown when the "help" icon is clicked.
12617 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12618 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12620 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12623 // Configuration initialization
12624 config
= config
|| {};
12626 // Parent constructor
12627 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12629 // Mixin constructors
12630 OO
.ui
.mixin
.IconElement
.call( this, config
);
12631 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12632 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12635 this.$header
= $( '<legend>' );
12639 .addClass( 'oo-ui-fieldsetLayout-header' )
12640 .append( this.$icon
, this.$label
);
12641 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12643 .addClass( 'oo-ui-fieldsetLayout' )
12644 .prepend( this.$header
, this.$group
);
12647 if ( config
.help
) {
12648 if ( config
.helpInline
) {
12649 helpWidget
= new OO
.ui
.LabelWidget( {
12650 label
: config
.help
,
12651 classes
: [ 'oo-ui-inline-help' ]
12653 this.$element
.prepend( this.$header
, helpWidget
.$element
, this.$group
);
12655 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12656 $overlay
: config
.$overlay
,
12660 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12663 label
: OO
.ui
.msg( 'ooui-field-help' ),
12664 invisibleLabel
: true
12666 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12667 helpWidget
.getPopup().$body
.html( config
.help
.toString() );
12669 helpWidget
.getPopup().$body
.text( config
.help
);
12671 this.$header
.append( helpWidget
.$element
);
12674 if ( Array
.isArray( config
.items
) ) {
12675 this.addItems( config
.items
);
12681 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12682 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12683 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12684 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12686 /* Static Properties */
12692 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12695 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use
12696 * browser-based form submission for the fields instead of handling them in JavaScript. Form layouts
12697 * can be configured with an HTML form action, an encoding type, and a method using the #action,
12698 * #enctype, and #method configs, respectively.
12699 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12701 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12702 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12703 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12704 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12705 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12706 * often have simplified APIs to match the capabilities of HTML forms.
12707 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12709 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12710 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12713 * // Example of a form layout that wraps a fieldset layout.
12714 * var input1 = new OO.ui.TextInputWidget( {
12715 * placeholder: 'Username'
12717 * input2 = new OO.ui.TextInputWidget( {
12718 * placeholder: 'Password',
12721 * submit = new OO.ui.ButtonInputWidget( {
12724 * fieldset = new OO.ui.FieldsetLayout( {
12725 * label: 'A form layout'
12728 * fieldset.addItems( [
12729 * new OO.ui.FieldLayout( input1, {
12730 * label: 'Username',
12733 * new OO.ui.FieldLayout( input2, {
12734 * label: 'Password',
12737 * new OO.ui.FieldLayout( submit )
12739 * var form = new OO.ui.FormLayout( {
12740 * items: [ fieldset ],
12741 * action: '/api/formhandler',
12744 * $( document.body ).append( form.$element );
12747 * @extends OO.ui.Layout
12748 * @mixins OO.ui.mixin.GroupElement
12751 * @param {Object} [config] Configuration options
12752 * @cfg {string} [method] HTML form `method` attribute
12753 * @cfg {string} [action] HTML form `action` attribute
12754 * @cfg {string} [enctype] HTML form `enctype` attribute
12755 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12757 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12760 // Configuration initialization
12761 config
= config
|| {};
12763 // Parent constructor
12764 OO
.ui
.FormLayout
.parent
.call( this, config
);
12766 // Mixin constructors
12767 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12770 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12772 // Make sure the action is safe
12773 action
= config
.action
;
12774 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12775 action
= './' + action
;
12780 .addClass( 'oo-ui-formLayout' )
12782 method
: config
.method
,
12784 enctype
: config
.enctype
12786 if ( Array
.isArray( config
.items
) ) {
12787 this.addItems( config
.items
);
12793 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12794 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12799 * A 'submit' event is emitted when the form is submitted.
12804 /* Static Properties */
12810 OO
.ui
.FormLayout
.static.tagName
= 'form';
12815 * Handle form submit events.
12818 * @param {jQuery.Event} e Submit event
12820 * @return {OO.ui.FormLayout} The layout, for chaining
12822 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12823 if ( this.emit( 'submit' ) ) {
12829 * PanelLayouts expand to cover the entire area of their parent. They can be configured with
12830 * scrolling, padding, and a frame, and are often used together with
12831 * {@link OO.ui.StackLayout StackLayouts}.
12834 * // Example of a panel layout
12835 * var panel = new OO.ui.PanelLayout( {
12839 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12841 * $( document.body ).append( panel.$element );
12844 * @extends OO.ui.Layout
12847 * @param {Object} [config] Configuration options
12848 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12849 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12850 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12851 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside
12854 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12855 // Configuration initialization
12856 config
= $.extend( {
12863 // Parent constructor
12864 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12867 this.$element
.addClass( 'oo-ui-panelLayout' );
12868 if ( config
.scrollable
) {
12869 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12871 if ( config
.padded
) {
12872 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12874 if ( config
.expanded
) {
12875 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12877 if ( config
.framed
) {
12878 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12884 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12886 /* Static Methods */
12891 OO
.ui
.PanelLayout
.static.reusePreInfuseDOM = function ( node
, config
) {
12892 config
= OO
.ui
.PanelLayout
.parent
.static.reusePreInfuseDOM( node
, config
);
12893 if ( config
.preserveContent
!== false ) {
12894 config
.$content
= $( node
).contents();
12902 * Focus the panel layout
12904 * The default implementation just focuses the first focusable element in the panel
12906 OO
.ui
.PanelLayout
.prototype.focus = function () {
12907 OO
.ui
.findFocusable( this.$element
).focus();
12911 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12912 * items), with small margins between them. Convenient when you need to put a number of block-level
12913 * widgets on a single line next to each other.
12915 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12918 * // HorizontalLayout with a text input and a label.
12919 * var layout = new OO.ui.HorizontalLayout( {
12921 * new OO.ui.LabelWidget( { label: 'Label' } ),
12922 * new OO.ui.TextInputWidget( { value: 'Text' } )
12925 * $( document.body ).append( layout.$element );
12928 * @extends OO.ui.Layout
12929 * @mixins OO.ui.mixin.GroupElement
12932 * @param {Object} [config] Configuration options
12933 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12935 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12936 // Configuration initialization
12937 config
= config
|| {};
12939 // Parent constructor
12940 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12942 // Mixin constructors
12943 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12946 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12947 if ( Array
.isArray( config
.items
) ) {
12948 this.addItems( config
.items
);
12954 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12955 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12958 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12959 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12960 * (to adjust the value in increments) to allow the user to enter a number.
12963 * // A NumberInputWidget.
12964 * var numberInput = new OO.ui.NumberInputWidget( {
12965 * label: 'NumberInputWidget',
12966 * input: { value: 5 },
12970 * $( document.body ).append( numberInput.$element );
12973 * @extends OO.ui.TextInputWidget
12976 * @param {Object} [config] Configuration options
12977 * @cfg {Object} [minusButton] Configuration options to pass to the
12978 * {@link OO.ui.ButtonWidget decrementing button widget}.
12979 * @cfg {Object} [plusButton] Configuration options to pass to the
12980 * {@link OO.ui.ButtonWidget incrementing button widget}.
12981 * @cfg {number} [min=-Infinity] Minimum allowed value
12982 * @cfg {number} [max=Infinity] Maximum allowed value
12983 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12984 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or Up/Down arrow keys.
12985 * Defaults to `step` if specified, otherwise `1`.
12986 * @cfg {number} [pageStep=10*buttonStep] Delta when using the Page-up/Page-down keys.
12987 * Defaults to 10 times `buttonStep`.
12988 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12990 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12991 var $field
= $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' );
12993 // Configuration initialization
12994 config
= $.extend( {
13000 // For backward compatibility
13001 $.extend( config
, config
.input
);
13004 // Parent constructor
13005 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
13009 if ( config
.showButtons
) {
13010 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
13012 disabled
: this.isDisabled(),
13014 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
13019 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
13020 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
13022 disabled
: this.isDisabled(),
13024 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
13029 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
13034 keydown
: this.onKeyDown
.bind( this ),
13035 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
13037 if ( config
.showButtons
) {
13038 this.plusButton
.connect( this, {
13039 click
: [ 'onButtonClick', +1 ]
13041 this.minusButton
.connect( this, {
13042 click
: [ 'onButtonClick', -1 ]
13047 $field
.append( this.$input
);
13048 if ( config
.showButtons
) {
13050 .prepend( this.minusButton
.$element
)
13051 .append( this.plusButton
.$element
);
13055 if ( config
.allowInteger
|| config
.isInteger
) {
13056 // Backward compatibility
13059 this.setRange( config
.min
, config
.max
);
13060 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
13061 // Set the validation method after we set step and range
13062 // so that it doesn't immediately call setValidityFlag
13063 this.setValidation( this.validateNumber
.bind( this ) );
13066 .addClass( 'oo-ui-numberInputWidget' )
13067 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
13073 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
13077 // Backward compatibility
13078 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
13079 this.setStep( flag
? 1 : null );
13081 // Backward compatibility
13082 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
13084 // Backward compatibility
13085 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
13086 return this.step
=== 1;
13088 // Backward compatibility
13089 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
13092 * Set the range of allowed values
13094 * @param {number} min Minimum allowed value
13095 * @param {number} max Maximum allowed value
13097 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
13099 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
13103 this.$input
.attr( 'min', this.min
);
13104 this.$input
.attr( 'max', this.max
);
13105 this.setValidityFlag();
13109 * Get the current range
13111 * @return {number[]} Minimum and maximum values
13113 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
13114 return [ this.min
, this.max
];
13118 * Set the stepping deltas
13120 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
13121 * Defaults to `step` if specified, otherwise `1`.
13122 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
13123 * Defaults to 10 times `buttonStep`.
13124 * @param {number|null} [step] If specified, the field only accepts values that are multiples
13127 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
13128 if ( buttonStep
=== undefined ) {
13129 buttonStep
= step
|| 1;
13131 if ( pageStep
=== undefined ) {
13132 pageStep
= 10 * buttonStep
;
13134 if ( step
!== null && step
<= 0 ) {
13135 throw new Error( 'Step value, if given, must be positive' );
13137 if ( buttonStep
<= 0 ) {
13138 throw new Error( 'Button step value must be positive' );
13140 if ( pageStep
<= 0 ) {
13141 throw new Error( 'Page step value must be positive' );
13144 this.buttonStep
= buttonStep
;
13145 this.pageStep
= pageStep
;
13146 this.$input
.attr( 'step', this.step
|| 'any' );
13147 this.setValidityFlag();
13153 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
13154 if ( value
=== '' ) {
13155 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
13156 // so here we make sure an 'empty' value is actually displayed as such.
13157 this.$input
.val( '' );
13159 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
13163 * Get the current stepping values
13165 * @return {number[]} Button step, page step, and validity step
13167 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
13168 return [ this.buttonStep
, this.pageStep
, this.step
];
13172 * Get the current value of the widget as a number
13174 * @return {number} May be NaN, or an invalid number
13176 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
13177 return +this.getValue();
13181 * Adjust the value of the widget
13183 * @param {number} delta Adjustment amount
13185 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
13186 var n
, v
= this.getNumericValue();
13189 if ( isNaN( delta
) || !isFinite( delta
) ) {
13190 throw new Error( 'Delta must be a finite number' );
13193 if ( isNaN( v
) ) {
13197 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
13199 n
= Math
.round( n
/ this.step
) * this.step
;
13204 this.setValue( n
);
13211 * @param {string} value Field value
13212 * @return {boolean}
13214 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
13216 if ( value
=== '' ) {
13217 return !this.isRequired();
13220 if ( isNaN( n
) || !isFinite( n
) ) {
13224 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
13228 if ( n
< this.min
|| n
> this.max
) {
13236 * Handle mouse click events.
13239 * @param {number} dir +1 or -1
13241 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
13242 this.adjustValue( dir
* this.buttonStep
);
13246 * Handle mouse wheel events.
13249 * @param {jQuery.Event} event
13250 * @return {undefined|boolean} False to prevent default if event is handled
13252 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
13255 if ( this.isDisabled() || this.isReadOnly() ) {
13259 if ( this.$input
.is( ':focus' ) ) {
13260 // Standard 'wheel' event
13261 if ( event
.originalEvent
.deltaMode
!== undefined ) {
13262 this.sawWheelEvent
= true;
13264 if ( event
.originalEvent
.deltaY
) {
13265 delta
= -event
.originalEvent
.deltaY
;
13266 } else if ( event
.originalEvent
.deltaX
) {
13267 delta
= event
.originalEvent
.deltaX
;
13270 // Non-standard events
13271 if ( !this.sawWheelEvent
) {
13272 if ( event
.originalEvent
.wheelDeltaX
) {
13273 delta
= -event
.originalEvent
.wheelDeltaX
;
13274 } else if ( event
.originalEvent
.wheelDeltaY
) {
13275 delta
= event
.originalEvent
.wheelDeltaY
;
13276 } else if ( event
.originalEvent
.wheelDelta
) {
13277 delta
= event
.originalEvent
.wheelDelta
;
13278 } else if ( event
.originalEvent
.detail
) {
13279 delta
= -event
.originalEvent
.detail
;
13284 delta
= delta
< 0 ? -1 : 1;
13285 this.adjustValue( delta
* this.buttonStep
);
13293 * Handle key down events.
13296 * @param {jQuery.Event} e Key down event
13297 * @return {undefined|boolean} False to prevent default if event is handled
13299 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
13300 if ( this.isDisabled() || this.isReadOnly() ) {
13304 switch ( e
.which
) {
13305 case OO
.ui
.Keys
.UP
:
13306 this.adjustValue( this.buttonStep
);
13308 case OO
.ui
.Keys
.DOWN
:
13309 this.adjustValue( -this.buttonStep
);
13311 case OO
.ui
.Keys
.PAGEUP
:
13312 this.adjustValue( this.pageStep
);
13314 case OO
.ui
.Keys
.PAGEDOWN
:
13315 this.adjustValue( -this.pageStep
);
13321 * Update the disabled state of the controls
13325 * @return {OO.ui.NumberInputWidget} The widget, for chaining
13327 OO
.ui
.NumberInputWidget
.prototype.updateControlsDisabled = function () {
13328 var disabled
= this.isDisabled() || this.isReadOnly();
13329 if ( this.minusButton
) {
13330 this.minusButton
.setDisabled( disabled
);
13332 if ( this.plusButton
) {
13333 this.plusButton
.setDisabled( disabled
);
13341 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
13343 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13344 this.updateControlsDisabled();
13351 OO
.ui
.NumberInputWidget
.prototype.setReadOnly = function () {
13353 OO
.ui
.NumberInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
13354 this.updateControlsDisabled();
13359 * SelectFileInputWidgets allow for selecting files, using <input type="file">. These
13360 * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
13361 * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
13362 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
13364 * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename.
13367 * // A file select input widget.
13368 * var selectFile = new OO.ui.SelectFileInputWidget();
13369 * $( document.body ).append( selectFile.$element );
13371 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
13374 * @extends OO.ui.InputWidget
13377 * @param {Object} [config] Configuration options
13378 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
13379 * @cfg {boolean} [multiple=false] Allow multiple files to be selected.
13380 * @cfg {string} [placeholder] Text to display when no file is selected.
13381 * @cfg {Object} [button] Config to pass to select file button.
13382 * @cfg {string} [icon] Icon to show next to file info
13384 OO
.ui
.SelectFileInputWidget
= function OoUiSelectFileInputWidget( config
) {
13387 config
= config
|| {};
13389 // Construct buttons before parent method is called (calling setDisabled)
13390 this.selectButton
= new OO
.ui
.ButtonWidget( $.extend( {
13391 $element
: $( '<label>' ),
13392 classes
: [ 'oo-ui-selectFileInputWidget-selectButton' ],
13393 label
: OO
.ui
.msg( 'ooui-selectfile-button-select' )
13394 }, config
.button
) );
13396 // Configuration initialization
13397 config
= $.extend( {
13399 placeholder
: OO
.ui
.msg( 'ooui-selectfile-placeholder' ),
13400 $tabIndexed
: this.selectButton
.$tabIndexed
13403 this.info
= new OO
.ui
.SearchInputWidget( {
13404 classes
: [ 'oo-ui-selectFileInputWidget-info' ],
13405 placeholder
: config
.placeholder
,
13406 // Pass an empty collection so that .focus() always does nothing
13407 $tabIndexed
: $( [] )
13408 } ).setIcon( config
.icon
);
13409 // Set tabindex manually on $input as $tabIndexed has been overridden
13410 this.info
.$input
.attr( 'tabindex', -1 );
13412 // Parent constructor
13413 OO
.ui
.SelectFileInputWidget
.parent
.call( this, config
);
13416 this.currentFiles
= this.filterFiles( this.$input
[ 0 ].files
|| [] );
13417 if ( Array
.isArray( config
.accept
) ) {
13418 this.accept
= config
.accept
;
13420 this.accept
= null;
13422 this.multiple
= !!config
.multiple
;
13425 this.info
.connect( this, { change
: 'onInfoChange' } );
13426 this.selectButton
.$button
.on( {
13427 keypress
: this.onKeyPress
.bind( this )
13430 change
: this.onFileSelected
.bind( this ),
13432 // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
13433 // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
13434 // Since this messes with our custom styling (the file input has large dimensions and this
13435 // causes the label to scroll out of view), scroll the button back to top. (T192131)
13436 focus: function () {
13437 widget
.$input
.parent().prop( 'scrollTop', 0 );
13440 this.connect( this, { change
: 'updateUI' } );
13442 this.fieldLayout
= new OO
.ui
.ActionFieldLayout( this.info
, this.selectButton
, { align
: 'top' } );
13447 // this.selectButton is tabindexed
13449 // Infused input may have previously by
13450 // TabIndexed, so remove aria-disabled attr.
13451 'aria-disabled': null
13454 if ( this.accept
) {
13455 this.$input
.attr( 'accept', this.accept
.join( ', ' ) );
13457 if ( this.multiple
) {
13458 this.$input
.attr( 'multiple', '' );
13460 this.selectButton
.$button
.append( this.$input
);
13463 .addClass( 'oo-ui-selectFileInputWidget' )
13464 .append( this.fieldLayout
.$element
);
13471 OO
.inheritClass( OO
.ui
.SelectFileInputWidget
, OO
.ui
.InputWidget
);
13473 /* Static properties */
13475 // Set empty title so that browser default tooltips like "No file chosen" don't appear.
13476 // On SelectFileWidget this tooltip will often be incorrect, so create a consistent
13477 // experience on SelectFileInputWidget.
13478 OO
.ui
.SelectFileInputWidget
.static.title
= '';
13483 * Get the filename of the currently selected file.
13485 * @return {string} Filename
13487 OO
.ui
.SelectFileInputWidget
.prototype.getFilename = function () {
13488 if ( this.currentFiles
.length
) {
13489 return this.currentFiles
.map( function ( file
) {
13493 // Try to strip leading fakepath.
13494 return this.getValue().split( '\\' ).pop();
13501 OO
.ui
.SelectFileInputWidget
.prototype.setValue = function ( value
) {
13502 if ( value
=== undefined ) {
13503 // Called during init, don't replace value if just infusing.
13507 // We need to update this.value, but without trying to modify
13508 // the DOM value, which would throw an exception.
13509 if ( this.value
!== value
) {
13510 this.value
= value
;
13511 this.emit( 'change', this.value
);
13514 this.currentFiles
= [];
13516 OO
.ui
.SelectFileInputWidget
.super.prototype.setValue
.call( this, '' );
13521 * Handle file selection from the input.
13524 * @param {jQuery.Event} e
13526 OO
.ui
.SelectFileInputWidget
.prototype.onFileSelected = function ( e
) {
13527 this.currentFiles
= this.filterFiles( e
.target
.files
|| [] );
13531 * Update the user interface when a file is selected or unselected.
13535 OO
.ui
.SelectFileInputWidget
.prototype.updateUI = function () {
13536 this.info
.setValue( this.getFilename() );
13540 * Determine if we should accept this file.
13543 * @param {FileList|File[]} files Files to filter
13544 * @return {File[]} Filter files
13546 OO
.ui
.SelectFileInputWidget
.prototype.filterFiles = function ( files
) {
13547 var accept
= this.accept
;
13549 function mimeAllowed( file
) {
13551 mimeType
= file
.type
;
13553 if ( !accept
|| !mimeType
) {
13557 for ( i
= 0; i
< accept
.length
; i
++ ) {
13558 mimeTest
= accept
[ i
];
13559 if ( mimeTest
=== mimeType
) {
13561 } else if ( mimeTest
.substr( -2 ) === '/*' ) {
13562 mimeTest
= mimeTest
.substr( 0, mimeTest
.length
- 1 );
13563 if ( mimeType
.substr( 0, mimeTest
.length
) === mimeTest
) {
13571 return Array
.prototype.filter
.call( files
, mimeAllowed
);
13575 * Handle info input change events
13577 * The info widget can only be changed by the user
13578 * with the clear button.
13581 * @param {string} value
13583 OO
.ui
.SelectFileInputWidget
.prototype.onInfoChange = function ( value
) {
13584 if ( value
=== '' ) {
13585 this.setValue( null );
13590 * Handle key press events.
13593 * @param {jQuery.Event} e Key press event
13594 * @return {undefined|boolean} False to prevent default if event is handled
13596 OO
.ui
.SelectFileInputWidget
.prototype.onKeyPress = function ( e
) {
13597 if ( !this.isDisabled() && this.$input
&&
13598 ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
13600 // Emit a click to open the file selector.
13601 this.$input
.trigger( 'click' );
13602 // Taking focus from the selectButton means keyUp isn't fired, so fire it manually.
13603 this.selectButton
.onDocumentKeyUp( e
);
13611 OO
.ui
.SelectFileInputWidget
.prototype.setDisabled = function ( disabled
) {
13613 OO
.ui
.SelectFileInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13615 this.selectButton
.setDisabled( disabled
);
13616 this.info
.setDisabled( disabled
);
13623 //# sourceMappingURL=oojs-ui-core.js.map.json