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-03-26T23:00:40Z
16 * Namespace for all classes, static methods and static properties.
48 * Constants for MouseEvent.which
52 OO
.ui
.MouseButtons
= {
65 * Generate a unique ID for element
69 OO
.ui
.generateElementId = function () {
71 return 'ooui-' + OO
.ui
.elementId
;
75 * Check if an element is focusable.
76 * Inspired by :focusable in jQueryUI v1.11.4 - 2015-04-14
78 * @param {jQuery} $element Element to test
79 * @return {boolean} Element is focusable
81 OO
.ui
.isFocusableElement = function ( $element
) {
83 element
= $element
[ 0 ];
85 // Anything disabled is not focusable
86 if ( element
.disabled
) {
90 // Check if the element is visible
92 // This is quicker than calling $element.is( ':visible' )
93 $.expr
.pseudos
.visible( element
) &&
94 // Check that all parents are visible
95 !$element
.parents().addBack().filter( function () {
96 return $.css( this, 'visibility' ) === 'hidden';
102 // Check if the element is ContentEditable, which is the string 'true'
103 if ( element
.contentEditable
=== 'true' ) {
107 // Anything with a non-negative numeric tabIndex is focusable.
108 // Use .prop to avoid browser bugs
109 if ( $element
.prop( 'tabIndex' ) >= 0 ) {
113 // Some element types are naturally focusable
114 // (indexOf is much faster than regex in Chrome and about the
115 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
116 nodeName
= element
.nodeName
.toLowerCase();
117 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName
) !== -1 ) {
121 // Links and areas are focusable if they have an href
122 if ( ( nodeName
=== 'a' || nodeName
=== 'area' ) && $element
.attr( 'href' ) !== undefined ) {
130 * Find a focusable child.
132 * @param {jQuery} $container Container to search in
133 * @param {boolean} [backwards] Search backwards
134 * @return {jQuery} Focusable child, or an empty jQuery object if none found
136 OO
.ui
.findFocusable = function ( $container
, backwards
) {
137 var $focusable
= $( [] ),
138 // $focusableCandidates is a superset of things that
139 // could get matched by isFocusableElement
140 $focusableCandidates
= $container
141 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
144 $focusableCandidates
= Array
.prototype.reverse
.call( $focusableCandidates
);
147 $focusableCandidates
.each( function () {
148 var $this = $( this );
149 if ( OO
.ui
.isFocusableElement( $this ) ) {
158 * Get the user's language and any fallback languages.
160 * These language codes are used to localize user interface elements in the user's language.
162 * In environments that provide a localization system, this function should be overridden to
163 * return the user's language(s). The default implementation returns English (en) only.
165 * @return {string[]} Language codes, in descending order of priority
167 OO
.ui
.getUserLanguages = function () {
172 * Get a value in an object keyed by language code.
174 * @param {Object.<string,Mixed>} obj Object keyed by language code
175 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
176 * @param {string} [fallback] Fallback code, used if no matching language can be found
177 * @return {Mixed} Local value
179 OO
.ui
.getLocalValue = function ( obj
, lang
, fallback
) {
182 // Requested language
186 // Known user language
187 langs
= OO
.ui
.getUserLanguages();
188 for ( i
= 0, len
= langs
.length
; i
< len
; i
++ ) {
195 if ( obj
[ fallback
] ) {
196 return obj
[ fallback
];
198 // First existing language
199 for ( lang
in obj
) {
207 * Check if a node is contained within another node.
209 * Similar to jQuery#contains except a list of containers can be supplied
210 * and a boolean argument allows you to include the container in the match list
212 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
213 * @param {HTMLElement} contained Node to find
214 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match,
215 * otherwise only match descendants
216 * @return {boolean} The node is in the list of target nodes
218 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
220 if ( !Array
.isArray( containers
) ) {
221 containers
= [ containers
];
223 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
225 ( matchContainers
&& contained
=== containers
[ i
] ) ||
226 $.contains( containers
[ i
], contained
)
235 * Return a function, that, as long as it continues to be invoked, will not
236 * be triggered. The function will be called after it stops being called for
237 * N milliseconds. If `immediate` is passed, trigger the function on the
238 * leading edge, instead of the trailing.
240 * Ported from: http://underscorejs.org/underscore.js
242 * @param {Function} func Function to debounce
243 * @param {number} [wait=0] Wait period in milliseconds
244 * @param {boolean} [immediate] Trigger on leading edge
245 * @return {Function} Debounced function
247 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
252 later = function () {
255 func
.apply( context
, args
);
258 if ( immediate
&& !timeout
) {
259 func
.apply( context
, args
);
261 if ( !timeout
|| wait
) {
262 clearTimeout( timeout
);
263 timeout
= setTimeout( later
, wait
);
269 * Puts a console warning with provided message.
271 * @param {string} message Message
273 OO
.ui
.warnDeprecation = function ( message
) {
274 if ( OO
.getProp( window
, 'console', 'warn' ) !== undefined ) {
275 // eslint-disable-next-line no-console
276 console
.warn( message
);
281 * Returns a function, that, when invoked, will only be triggered at most once
282 * during a given window of time. If called again during that window, it will
283 * wait until the window ends and then trigger itself again.
285 * As it's not knowable to the caller whether the function will actually run
286 * when the wrapper is called, return values from the function are entirely
289 * @param {Function} func Function to throttle
290 * @param {number} wait Throttle window length, in milliseconds
291 * @return {Function} Throttled function
293 OO
.ui
.throttle = function ( func
, wait
) {
294 var context
, args
, timeout
,
298 previous
= Date
.now();
299 func
.apply( context
, args
);
302 // Check how long it's been since the last time the function was
303 // called, and whether it's more or less than the requested throttle
304 // period. If it's less, run the function immediately. If it's more,
305 // set a timeout for the remaining time -- but don't replace an
306 // existing timeout, since that'd indefinitely prolong the wait.
307 var remaining
= wait
- ( Date
.now() - previous
);
310 if ( remaining
<= 0 ) {
311 // Note: unless wait was ridiculously large, this means we'll
312 // automatically run the first time the function was called in a
313 // given period. (If you provide a wait period larger than the
314 // current Unix timestamp, you *deserve* unexpected behavior.)
315 clearTimeout( timeout
);
317 } else if ( !timeout
) {
318 timeout
= setTimeout( run
, remaining
);
324 * A (possibly faster) way to get the current timestamp as an integer.
326 * @deprecated Since 0.31.1; use `Date.now()` instead.
327 * @return {number} Current timestamp, in milliseconds since the Unix epoch
329 OO
.ui
.now = function () {
330 OO
.ui
.warnDeprecation( 'OO.ui.now() is deprecated, use Date.now() instead' );
335 * Reconstitute a JavaScript object corresponding to a widget created by
336 * the PHP implementation.
338 * This is an alias for `OO.ui.Element.static.infuse()`.
340 * @param {string|HTMLElement|jQuery} idOrNode
341 * A DOM id (if a string) or node for the widget to infuse.
342 * @param {Object} [config] Configuration options
343 * @return {OO.ui.Element}
344 * The `OO.ui.Element` corresponding to this (infusable) document node.
346 OO
.ui
.infuse = function ( idOrNode
, config
) {
347 return OO
.ui
.Element
.static.infuse( idOrNode
, config
);
352 * Message store for the default implementation of OO.ui.msg.
354 * Environments that provide a localization system should not use this, but should override
355 * OO.ui.msg altogether.
360 // Tool tip for a button that moves items in a list down one place
361 'ooui-outline-control-move-down': 'Move item down',
362 // Tool tip for a button that moves items in a list up one place
363 'ooui-outline-control-move-up': 'Move item up',
364 // Tool tip for a button that removes items from a list
365 'ooui-outline-control-remove': 'Remove item',
366 // Label for the toolbar group that contains a list of all other available tools
367 'ooui-toolbar-more': 'More',
368 // Label for the fake tool that expands the full list of tools in a toolbar group
369 'ooui-toolgroup-expand': 'More',
370 // Label for the fake tool that collapses the full list of tools in a toolbar group
371 'ooui-toolgroup-collapse': 'Fewer',
372 // Default label for the tooltip for the button that removes a tag item
373 'ooui-item-remove': 'Remove',
374 // Default label for the accept button of a confirmation dialog
375 'ooui-dialog-message-accept': 'OK',
376 // Default label for the reject button of a confirmation dialog
377 'ooui-dialog-message-reject': 'Cancel',
378 // Title for process dialog error description
379 'ooui-dialog-process-error': 'Something went wrong',
380 // Label for process dialog dismiss error button, visible when describing errors
381 'ooui-dialog-process-dismiss': 'Dismiss',
382 // Label for process dialog retry action button, visible when describing only recoverable
384 'ooui-dialog-process-retry': 'Try again',
385 // Label for process dialog retry action button, visible when describing only warnings
386 'ooui-dialog-process-continue': 'Continue',
387 // Label for button in combobox input that triggers its dropdown
388 'ooui-combobox-button-label': 'Dropdown for combobox',
389 // Label for the file selection widget's select file button
390 'ooui-selectfile-button-select': 'Select a file',
391 // Label for the file selection widget if file selection is not supported
392 'ooui-selectfile-not-supported': 'File selection is not supported',
393 // Label for the file selection widget when no file is currently selected
394 'ooui-selectfile-placeholder': 'No file is selected',
395 // Label for the file selection widget's drop target
396 'ooui-selectfile-dragdrop-placeholder': 'Drop file here',
397 // Label for the help icon attached to a form field
398 'ooui-field-help': 'Help'
402 * Get a localized message.
404 * After the message key, message parameters may optionally be passed. In the default
405 * implementation, any occurrences of $1 are replaced with the first parameter, $2 with the
406 * second parameter, etc.
407 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long
408 * as they support unnamed, ordered message parameters.
410 * In environments that provide a localization system, this function should be overridden to
411 * return the message translated in the user's language. The default implementation always
412 * returns English messages. An example of doing this with
413 * [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) follows.
416 * var i, iLen, button,
417 * messagePath = 'oojs-ui/dist/i18n/',
418 * languages = [ $.i18n().locale, 'ur', 'en' ],
421 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
422 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
425 * $.i18n().load( languageMap ).done( function() {
426 * // Replace the built-in `msg` only once we've loaded the internationalization.
427 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
428 * // you put off creating any widgets until this promise is complete, no English
429 * // will be displayed.
430 * OO.ui.msg = $.i18n;
432 * // A button displaying "OK" in the default locale
433 * button = new OO.ui.ButtonWidget( {
434 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
437 * $( document.body ).append( button.$element );
439 * // A button displaying "OK" in Urdu
440 * $.i18n().locale = 'ur';
441 * button = new OO.ui.ButtonWidget( {
442 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
445 * $( document.body ).append( button.$element );
448 * @param {string} key Message key
449 * @param {...Mixed} [params] Message parameters
450 * @return {string} Translated message with parameters substituted
452 OO
.ui
.msg = function ( key
) {
453 var message
= messages
[ key
],
454 params
= Array
.prototype.slice
.call( arguments
, 1 );
455 if ( typeof message
=== 'string' ) {
456 // Perform $1 substitution
457 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
458 var i
= parseInt( n
, 10 );
459 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
462 // Return placeholder if message not found
463 message
= '[' + key
+ ']';
470 * Package a message and arguments for deferred resolution.
472 * Use this when you are statically specifying a message and the message may not yet be present.
474 * @param {string} key Message key
475 * @param {...Mixed} [params] Message parameters
476 * @return {Function} Function that returns the resolved message when executed
478 OO
.ui
.deferMsg = function () {
479 var args
= arguments
;
481 return OO
.ui
.msg
.apply( OO
.ui
, args
);
488 * If the message is a function it will be executed, otherwise it will pass through directly.
490 * @param {Function|string} msg Deferred message, or message text
491 * @return {string} Resolved message
493 OO
.ui
.resolveMsg = function ( msg
) {
494 if ( typeof msg
=== 'function' ) {
501 * @param {string} url
504 OO
.ui
.isSafeUrl = function ( url
) {
505 // Keep this function in sync with php/Tag.php
506 var i
, protocolWhitelist
;
508 function stringStartsWith( haystack
, needle
) {
509 return haystack
.substr( 0, needle
.length
) === needle
;
512 protocolWhitelist
= [
513 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
514 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
515 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
522 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
523 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
528 // This matches '//' too
529 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
532 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
540 * Check if the user has a 'mobile' device.
542 * For our purposes this means the user is primarily using an
543 * on-screen keyboard, touch input instead of a mouse and may
544 * have a physically small display.
546 * It is left up to implementors to decide how to compute this
547 * so the default implementation always returns false.
549 * @return {boolean} User is on a mobile device
551 OO
.ui
.isMobile = function () {
556 * Get the additional spacing that should be taken into account when displaying elements that are
557 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
558 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
560 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
561 * the extra spacing from that edge of viewport (in pixels)
563 OO
.ui
.getViewportSpacing = function () {
573 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
574 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
576 * @return {jQuery} Default overlay node
578 OO
.ui
.getDefaultOverlay = function () {
579 if ( !OO
.ui
.$defaultOverlay
) {
580 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
581 $( document
.body
).append( OO
.ui
.$defaultOverlay
);
583 return OO
.ui
.$defaultOverlay
;
591 * Namespace for OOUI mixins.
593 * Mixins are named according to the type of object they are intended to
594 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
595 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
596 * is intended to be mixed in to an instance of OO.ui.Widget.
604 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
605 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not
606 * have events connected to them and can't be interacted with.
612 * @param {Object} [config] Configuration options
613 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are
614 * added to the top level (e.g., the outermost div) of the element. See the
615 * [OOUI documentation on MediaWiki][2] for an example.
616 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
617 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
618 * @cfg {string} [text] Text to insert
619 * @cfg {Array} [content] An array of content elements to append (after #text).
620 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
621 * Instances of OO.ui.Element will have their $element appended.
622 * @cfg {jQuery} [$content] Content elements to append (after #text).
623 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
624 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number,
626 * Data can also be specified with the #setData method.
628 OO
.ui
.Element
= function OoUiElement( config
) {
629 if ( OO
.ui
.isDemo
) {
630 this.initialConfig
= config
;
632 // Configuration initialization
633 config
= config
|| {};
636 this.$ = function () {
637 OO
.ui
.warnDeprecation( 'this.$ is deprecated, use global $ instead' );
638 return $.apply( this, arguments
);
640 this.elementId
= null;
642 this.data
= config
.data
;
643 this.$element
= config
.$element
||
644 $( document
.createElement( this.getTagName() ) );
645 this.elementGroup
= null;
648 if ( Array
.isArray( config
.classes
) ) {
649 this.$element
.addClass( config
.classes
);
652 this.setElementId( config
.id
);
655 this.$element
.text( config
.text
);
657 if ( config
.content
) {
658 // The `content` property treats plain strings as text; use an
659 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
660 // appropriate $element appended.
661 this.$element
.append( config
.content
.map( function ( v
) {
662 if ( typeof v
=== 'string' ) {
663 // Escape string so it is properly represented in HTML.
664 // Don't create empty text nodes for empty strings.
665 return v
? document
.createTextNode( v
) : undefined;
666 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
669 } else if ( v
instanceof OO
.ui
.Element
) {
675 if ( config
.$content
) {
676 // The `$content` property treats plain strings as HTML.
677 this.$element
.append( config
.$content
);
683 OO
.initClass( OO
.ui
.Element
);
685 /* Static Properties */
688 * The name of the HTML tag used by the element.
690 * The static value may be ignored if the #getTagName method is overridden.
696 OO
.ui
.Element
.static.tagName
= 'div';
701 * Reconstitute a JavaScript object corresponding to a widget created
702 * by the PHP implementation.
704 * @param {string|HTMLElement|jQuery} idOrNode
705 * A DOM id (if a string) or node for the widget to infuse.
706 * @param {Object} [config] Configuration options
707 * @return {OO.ui.Element}
708 * The `OO.ui.Element` corresponding to this (infusable) document node.
709 * For `Tag` objects emitted on the HTML side (used occasionally for content)
710 * the value returned is a newly-created Element wrapping around the existing
713 OO
.ui
.Element
.static.infuse = function ( idOrNode
, config
) {
714 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, config
, false );
716 if ( typeof idOrNode
=== 'string' ) {
717 // IDs deprecated since 0.29.7
718 OO
.ui
.warnDeprecation(
719 'Passing a string ID to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
722 // Verify that the type matches up.
723 // FIXME: uncomment after T89721 is fixed, see T90929.
725 if ( !( obj instanceof this['class'] ) ) {
726 throw new Error( 'Infusion type mismatch!' );
733 * Implementation helper for `infuse`; skips the type check and has an
734 * extra property so that only the top-level invocation touches the DOM.
737 * @param {string|HTMLElement|jQuery} idOrNode
738 * @param {Object} [config] Configuration options
739 * @param {jQuery.Promise} [domPromise] A promise that will be resolved
740 * when the top-level widget of this infusion is inserted into DOM,
741 * replacing the original node; only used internally.
742 * @return {OO.ui.Element}
744 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, config
, domPromise
) {
745 // look for a cached result of a previous infusion.
746 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
747 if ( typeof idOrNode
=== 'string' ) {
749 $elem
= $( document
.getElementById( id
) );
751 $elem
= $( idOrNode
);
752 id
= $elem
.attr( 'id' );
754 if ( !$elem
.length
) {
755 if ( typeof idOrNode
=== 'string' ) {
756 error
= 'Widget not found: ' + idOrNode
;
757 } else if ( idOrNode
&& idOrNode
.selector
) {
758 error
= 'Widget not found: ' + idOrNode
.selector
;
760 error
= 'Widget not found';
762 throw new Error( error
);
764 if ( $elem
[ 0 ].oouiInfused
) {
765 $elem
= $elem
[ 0 ].oouiInfused
;
767 data
= $elem
.data( 'ooui-infused' );
770 if ( data
=== true ) {
771 throw new Error( 'Circular dependency! ' + id
);
774 // Pick up dynamic state, like focus, value of form inputs, scroll position, etc.
775 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
776 // Restore dynamic state after the new element is re-inserted into DOM under
778 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
779 infusedChildren
= $elem
.data( 'ooui-infused-children' );
780 if ( infusedChildren
&& infusedChildren
.length
) {
781 infusedChildren
.forEach( function ( data
) {
782 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
783 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
789 data
= $elem
.attr( 'data-ooui' );
791 throw new Error( 'No infusion data found: ' + id
);
794 data
= JSON
.parse( data
);
798 if ( !( data
&& data
._
) ) {
799 throw new Error( 'No valid infusion data found: ' + id
);
801 if ( data
._
=== 'Tag' ) {
802 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
803 return new OO
.ui
.Element( $.extend( {}, config
, { $element
: $elem
} ) );
805 parts
= data
._
.split( '.' );
806 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
807 if ( cls
=== undefined ) {
808 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
811 // Verify that we're creating an OO.ui.Element instance
814 while ( parent
!== undefined ) {
815 if ( parent
=== OO
.ui
.Element
) {
820 parent
= parent
.parent
;
823 if ( parent
!== OO
.ui
.Element
) {
824 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
829 domPromise
= top
.promise();
831 $elem
.data( 'ooui-infused', true ); // prevent loops
832 data
.id
= id
; // implicit
833 infusedChildren
= [];
834 data
= OO
.copy( data
, null, function deserialize( value
) {
836 if ( OO
.isPlainObject( value
) ) {
838 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, config
, domPromise
);
839 infusedChildren
.push( infused
);
840 // Flatten the structure
841 infusedChildren
.push
.apply(
843 infused
.$element
.data( 'ooui-infused-children' ) || []
845 infused
.$element
.removeData( 'ooui-infused-children' );
848 if ( value
.html
!== undefined ) {
849 return new OO
.ui
.HtmlSnippet( value
.html
);
853 // allow widgets to reuse parts of the DOM
854 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
855 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
856 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
858 // eslint-disable-next-line new-cap
859 obj
= new cls( $.extend( {}, config
, data
) );
860 // If anyone is holding a reference to the old DOM element,
861 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
862 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
863 $elem
[ 0 ].oouiInfused
= obj
.$element
;
864 // now replace old DOM with this new DOM.
866 // An efficient constructor might be able to reuse the entire DOM tree of the original
867 // element, so only mutate the DOM if we need to.
868 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
869 $elem
.replaceWith( obj
.$element
);
873 obj
.$element
.data( 'ooui-infused', obj
);
874 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
875 // set the 'data-ooui' attribute so we can identify infused widgets
876 obj
.$element
.attr( 'data-ooui', '' );
877 // restore dynamic state after the new element is inserted into DOM
878 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
883 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
885 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
886 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
887 * constructor, which will be given the enhanced config.
890 * @param {HTMLElement} node
891 * @param {Object} config
894 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
899 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM
900 * node (and its children) that represent an Element of the same class and the given configuration,
901 * generated by the PHP implementation.
903 * This method is called just before `node` is detached from the DOM. The return value of this
904 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
905 * is inserted into DOM to replace `node`.
908 * @param {HTMLElement} node
909 * @param {Object} config
912 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
917 * Get a jQuery function within a specific document.
920 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
921 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
923 * @return {Function} Bound jQuery function
925 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
926 function wrapper( selector
) {
927 return $( selector
, wrapper
.context
);
930 wrapper
.context
= this.getDocument( context
);
933 wrapper
.$iframe
= $iframe
;
940 * Get the document of an element.
943 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
944 * @return {HTMLDocument|null} Document object
946 OO
.ui
.Element
.static.getDocument = function ( obj
) {
947 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
948 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
949 // Empty jQuery selections might have a context
956 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
961 * Get the window of an element or document.
964 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
965 * @return {Window} Window object
967 OO
.ui
.Element
.static.getWindow = function ( obj
) {
968 var doc
= this.getDocument( obj
);
969 return doc
.defaultView
;
973 * Get the direction of an element or document.
976 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
977 * @return {string} Text direction, either 'ltr' or 'rtl'
979 OO
.ui
.Element
.static.getDir = function ( obj
) {
982 if ( obj
instanceof $ ) {
985 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
986 isWin
= obj
.document
!== undefined;
987 if ( isDoc
|| isWin
) {
993 return $( obj
).css( 'direction' );
997 * Get the offset between two frames.
999 * TODO: Make this function not use recursion.
1002 * @param {Window} from Window of the child frame
1003 * @param {Window} [to=window] Window of the parent frame
1004 * @param {Object} [offset] Offset to start with, used internally
1005 * @return {Object} Offset object, containing left and top properties
1007 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
1008 var i
, len
, frames
, frame
, rect
;
1014 offset
= { top
: 0, left
: 0 };
1016 if ( from.parent
=== from ) {
1020 // Get iframe element
1021 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
1022 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
1023 if ( frames
[ i
].contentWindow
=== from ) {
1024 frame
= frames
[ i
];
1029 // Recursively accumulate offset values
1031 rect
= frame
.getBoundingClientRect();
1032 offset
.left
+= rect
.left
;
1033 offset
.top
+= rect
.top
;
1034 if ( from !== to
) {
1035 this.getFrameOffset( from.parent
, offset
);
1042 * Get the offset between two elements.
1044 * The two elements may be in a different frame, but in that case the frame $element is in must
1045 * be contained in the frame $anchor is in.
1048 * @param {jQuery} $element Element whose position to get
1049 * @param {jQuery} $anchor Element to get $element's position relative to
1050 * @return {Object} Translated position coordinates, containing top and left properties
1052 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1053 var iframe
, iframePos
,
1054 pos
= $element
.offset(),
1055 anchorPos
= $anchor
.offset(),
1056 elementDocument
= this.getDocument( $element
),
1057 anchorDocument
= this.getDocument( $anchor
);
1059 // If $element isn't in the same document as $anchor, traverse up
1060 while ( elementDocument
!== anchorDocument
) {
1061 iframe
= elementDocument
.defaultView
.frameElement
;
1063 throw new Error( '$element frame is not contained in $anchor frame' );
1065 iframePos
= $( iframe
).offset();
1066 pos
.left
+= iframePos
.left
;
1067 pos
.top
+= iframePos
.top
;
1068 elementDocument
= iframe
.ownerDocument
;
1070 pos
.left
-= anchorPos
.left
;
1071 pos
.top
-= anchorPos
.top
;
1076 * Get element border sizes.
1079 * @param {HTMLElement} el Element to measure
1080 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1082 OO
.ui
.Element
.static.getBorders = function ( el
) {
1083 var doc
= el
.ownerDocument
,
1084 win
= doc
.defaultView
,
1085 style
= win
.getComputedStyle( el
, null ),
1087 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1088 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1089 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1090 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1101 * Get dimensions of an element or window.
1104 * @param {HTMLElement|Window} el Element to measure
1105 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1107 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1109 doc
= el
.ownerDocument
|| el
.document
,
1110 win
= doc
.defaultView
;
1112 if ( win
=== el
|| el
=== doc
.documentElement
) {
1115 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1117 top
: $win
.scrollTop(),
1118 left
: $win
.scrollLeft()
1120 scrollbar
: { right
: 0, bottom
: 0 },
1124 bottom
: $win
.innerHeight(),
1125 right
: $win
.innerWidth()
1131 borders
: this.getBorders( el
),
1133 top
: $el
.scrollTop(),
1134 left
: $el
.scrollLeft()
1137 right
: $el
.innerWidth() - el
.clientWidth
,
1138 bottom
: $el
.innerHeight() - el
.clientHeight
1140 rect
: el
.getBoundingClientRect()
1146 * Get the number of pixels that an element's content is scrolled to the left.
1148 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1149 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1151 * This function smooths out browser inconsistencies (nicely described in the README at
1152 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1153 * with Firefox's 'scrollLeft', which seems the sanest.
1157 * @param {HTMLElement|Window} el Element to measure
1158 * @return {number} Scroll position from the left.
1159 * If the element's direction is LTR, this is a positive number between `0` (initial scroll
1160 * position) and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1161 * If the element's direction is RTL, this is a negative number between `0` (initial scroll
1162 * position) and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1164 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1165 var rtlScrollType
= null;
1168 var $definer
= $( '<div>' ).attr( {
1170 style
: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
1172 definer
= $definer
[ 0 ];
1174 $definer
.appendTo( 'body' );
1175 if ( definer
.scrollLeft
> 0 ) {
1177 rtlScrollType
= 'default';
1179 definer
.scrollLeft
= 1;
1180 if ( definer
.scrollLeft
=== 0 ) {
1181 // Firefox, old Opera
1182 rtlScrollType
= 'negative';
1184 // Internet Explorer, Edge
1185 rtlScrollType
= 'reverse';
1191 return function getScrollLeft( el
) {
1192 var isRoot
= el
.window
=== el
||
1193 el
=== el
.ownerDocument
.body
||
1194 el
=== el
.ownerDocument
.documentElement
,
1195 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1196 // All browsers use the correct scroll type ('negative') on the root, so don't
1197 // do any fixups when looking at the root element
1198 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1200 if ( direction
=== 'rtl' ) {
1201 if ( rtlScrollType
=== null ) {
1204 if ( rtlScrollType
=== 'reverse' ) {
1205 scrollLeft
= -scrollLeft
;
1206 } else if ( rtlScrollType
=== 'default' ) {
1207 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1216 * Get the root scrollable element of given element's document.
1218 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1219 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1220 * lets us use 'body' or 'documentElement' based on what is working.
1222 * https://code.google.com/p/chromium/issues/detail?id=303131
1225 * @param {HTMLElement} el Element to find root scrollable parent for
1226 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1227 * depending on browser
1229 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1230 var scrollTop
, body
;
1232 if ( OO
.ui
.scrollableElement
=== undefined ) {
1233 body
= el
.ownerDocument
.body
;
1234 scrollTop
= body
.scrollTop
;
1237 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1238 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1239 if ( Math
.round( body
.scrollTop
) === 1 ) {
1240 body
.scrollTop
= scrollTop
;
1241 OO
.ui
.scrollableElement
= 'body';
1243 OO
.ui
.scrollableElement
= 'documentElement';
1247 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1251 * Get closest scrollable container.
1253 * Traverses up until either a scrollable element or the root is reached, in which case the root
1254 * scrollable element will be returned (see #getRootScrollableElement).
1257 * @param {HTMLElement} el Element to find scrollable container for
1258 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1259 * @return {HTMLElement} Closest scrollable container
1261 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1263 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1264 // 'overflow-y' have different values, so we need to check the separate properties.
1265 props
= [ 'overflow-x', 'overflow-y' ],
1266 $parent
= $( el
).parent();
1268 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1269 props
= [ 'overflow-' + dimension
];
1272 // Special case for the document root (which doesn't really have any scrollable container,
1273 // since it is the ultimate scrollable container, but this is probably saner than null or
1275 if ( $( el
).is( 'html, body' ) ) {
1276 return this.getRootScrollableElement( el
);
1279 while ( $parent
.length
) {
1280 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1281 return $parent
[ 0 ];
1285 val
= $parent
.css( props
[ i
] );
1286 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will
1287 // never be scrolled in that direction, but they can actually be scrolled
1288 // programatically. The user can unintentionally perform a scroll in such case even if
1289 // the application doesn't scroll programatically, e.g. when jumping to an anchor, or
1290 // when using built-in find functionality.
1291 // This could cause funny issues...
1292 if ( val
=== 'auto' || val
=== 'scroll' ) {
1293 return $parent
[ 0 ];
1296 $parent
= $parent
.parent();
1298 // The element is unattached... return something mostly sane
1299 return this.getRootScrollableElement( el
);
1303 * Scroll element into view.
1306 * @param {HTMLElement} el Element to scroll into view
1307 * @param {Object} [config] Configuration options
1308 * @param {string} [config.duration='fast'] jQuery animation duration value
1309 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1310 * to scroll in both directions
1311 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1313 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1314 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
,
1316 deferred
= $.Deferred();
1318 // Configuration initialization
1319 config
= config
|| {};
1322 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1323 $container
= $( container
);
1324 elementDimensions
= this.getDimensions( el
);
1325 containerDimensions
= this.getDimensions( container
);
1326 $window
= $( this.getWindow( el
) );
1328 // Compute the element's position relative to the container
1329 if ( $container
.is( 'html, body' ) ) {
1330 // If the scrollable container is the root, this is easy
1332 top
: elementDimensions
.rect
.top
,
1333 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1334 left
: elementDimensions
.rect
.left
,
1335 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1338 // Otherwise, we have to subtract el's coordinates from container's coordinates
1340 top
: elementDimensions
.rect
.top
-
1341 ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1342 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
-
1343 containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1344 left
: elementDimensions
.rect
.left
-
1345 ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1346 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
-
1347 containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1351 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1352 if ( position
.top
< 0 ) {
1353 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1354 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1355 animations
.scrollTop
= containerDimensions
.scroll
.top
+
1356 Math
.min( position
.top
, -position
.bottom
);
1359 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1360 if ( position
.left
< 0 ) {
1361 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1362 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1363 animations
.scrollLeft
= containerDimensions
.scroll
.left
+
1364 Math
.min( position
.left
, -position
.right
);
1367 if ( !$.isEmptyObject( animations
) ) {
1368 // eslint-disable-next-line no-jquery/no-animate
1369 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ?
1370 'fast' : config
.duration
);
1371 $container
.queue( function ( next
) {
1378 return deferred
.promise();
1382 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1383 * and reserve space for them, because it probably doesn't.
1385 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1386 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1387 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a
1388 * reflow, and then reattach (or show) them back.
1391 * @param {HTMLElement} el Element to reconsider the scrollbars on
1393 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1394 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1395 // Save scroll position
1396 scrollLeft
= el
.scrollLeft
;
1397 scrollTop
= el
.scrollTop
;
1398 // Detach all children
1399 while ( el
.firstChild
) {
1400 nodes
.push( el
.firstChild
);
1401 el
.removeChild( el
.firstChild
);
1404 // eslint-disable-next-line no-void
1405 void el
.offsetHeight
;
1406 // Reattach all children
1407 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1408 el
.appendChild( nodes
[ i
] );
1410 // Restore scroll position (no-op if scrollbars disappeared)
1411 el
.scrollLeft
= scrollLeft
;
1412 el
.scrollTop
= scrollTop
;
1418 * Toggle visibility of an element.
1420 * @param {boolean} [show] Make element visible, omit to toggle visibility
1423 * @return {OO.ui.Element} The element, for chaining
1425 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1426 show
= show
=== undefined ? !this.visible
: !!show
;
1428 if ( show
!== this.isVisible() ) {
1429 this.visible
= show
;
1430 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1431 this.emit( 'toggle', show
);
1438 * Check if element is visible.
1440 * @return {boolean} element is visible
1442 OO
.ui
.Element
.prototype.isVisible = function () {
1443 return this.visible
;
1449 * @return {Mixed} Element data
1451 OO
.ui
.Element
.prototype.getData = function () {
1458 * @param {Mixed} data Element data
1460 * @return {OO.ui.Element} The element, for chaining
1462 OO
.ui
.Element
.prototype.setData = function ( data
) {
1468 * Set the element has an 'id' attribute.
1470 * @param {string} id
1472 * @return {OO.ui.Element} The element, for chaining
1474 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1475 this.elementId
= id
;
1476 this.$element
.attr( 'id', id
);
1481 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1482 * and return its value.
1486 OO
.ui
.Element
.prototype.getElementId = function () {
1487 if ( this.elementId
=== null ) {
1488 this.setElementId( OO
.ui
.generateElementId() );
1490 return this.elementId
;
1494 * Check if element supports one or more methods.
1496 * @param {string|string[]} methods Method or list of methods to check
1497 * @return {boolean} All methods are supported
1499 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1503 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1504 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1505 if ( typeof this[ methods
[ i
] ] === 'function' ) {
1510 return methods
.length
=== support
;
1514 * Update the theme-provided classes.
1516 * @localdoc This is called in element mixins and widget classes any time state changes.
1517 * Updating is debounced, minimizing overhead of changing multiple attributes and
1518 * guaranteeing that theme updates do not occur within an element's constructor
1520 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1521 OO
.ui
.theme
.queueUpdateElementClasses( this );
1525 * Get the HTML tag name.
1527 * Override this method to base the result on instance information.
1529 * @return {string} HTML tag name
1531 OO
.ui
.Element
.prototype.getTagName = function () {
1532 return this.constructor.static.tagName
;
1536 * Check if the element is attached to the DOM
1538 * @return {boolean} The element is attached to the DOM
1540 OO
.ui
.Element
.prototype.isElementAttached = function () {
1541 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1545 * Get the DOM document.
1547 * @return {HTMLDocument} Document object
1549 OO
.ui
.Element
.prototype.getElementDocument = function () {
1550 // Don't cache this in other ways either because subclasses could can change this.$element
1551 return OO
.ui
.Element
.static.getDocument( this.$element
);
1555 * Get the DOM window.
1557 * @return {Window} Window object
1559 OO
.ui
.Element
.prototype.getElementWindow = function () {
1560 return OO
.ui
.Element
.static.getWindow( this.$element
);
1564 * Get closest scrollable container.
1566 * @return {HTMLElement} Closest scrollable container
1568 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1569 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1573 * Get group element is in.
1575 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1577 OO
.ui
.Element
.prototype.getElementGroup = function () {
1578 return this.elementGroup
;
1582 * Set group element is in.
1584 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1586 * @return {OO.ui.Element} The element, for chaining
1588 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1589 this.elementGroup
= group
;
1594 * Scroll element into view.
1596 * @param {Object} [config] Configuration options
1597 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1599 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1601 !this.isElementAttached() ||
1602 !this.isVisible() ||
1603 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1605 return $.Deferred().resolve();
1607 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1611 * Restore the pre-infusion dynamic state for this widget.
1613 * This method is called after #$element has been inserted into DOM. The parameter is the return
1614 * value of #gatherPreInfuseState.
1617 * @param {Object} state
1619 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1623 * Wraps an HTML snippet for use with configuration values which default
1624 * to strings. This bypasses the default html-escaping done to string
1630 * @param {string} [content] HTML content
1632 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1634 this.content
= content
;
1639 OO
.initClass( OO
.ui
.HtmlSnippet
);
1646 * @return {string} Unchanged HTML snippet.
1648 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1649 return this.content
;
1653 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in
1654 * a way that is centrally controlled and can be updated dynamically. Layouts can be, and usually
1656 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout},
1657 * {@link OO.ui.FormLayout FormLayout}, {@link OO.ui.PanelLayout PanelLayout},
1658 * {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1659 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout}
1660 * for more information and examples.
1664 * @extends OO.ui.Element
1665 * @mixins OO.EventEmitter
1668 * @param {Object} [config] Configuration options
1670 OO
.ui
.Layout
= function OoUiLayout( config
) {
1671 // Configuration initialization
1672 config
= config
|| {};
1674 // Parent constructor
1675 OO
.ui
.Layout
.parent
.call( this, config
);
1677 // Mixin constructors
1678 OO
.EventEmitter
.call( this );
1681 this.$element
.addClass( 'oo-ui-layout' );
1686 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1687 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1692 * Reset scroll offsets
1695 * @return {OO.ui.Layout} The layout, for chaining
1697 OO
.ui
.Layout
.prototype.resetScroll = function () {
1698 this.$element
[ 0 ].scrollTop
= 0;
1699 // TODO: Reset scrollLeft in an RTL-aware manner, see OO.ui.Element.static.getScrollLeft.
1705 * Widgets are compositions of one or more OOUI elements that users can both view
1706 * and interact with. All widgets can be configured and modified via a standard API,
1707 * and their state can change dynamically according to a model.
1711 * @extends OO.ui.Element
1712 * @mixins OO.EventEmitter
1715 * @param {Object} [config] Configuration options
1716 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1717 * appearance reflects this state.
1719 OO
.ui
.Widget
= function OoUiWidget( config
) {
1720 // Initialize config
1721 config
= $.extend( { disabled
: false }, config
);
1723 // Parent constructor
1724 OO
.ui
.Widget
.parent
.call( this, config
);
1726 // Mixin constructors
1727 OO
.EventEmitter
.call( this );
1730 this.disabled
= null;
1731 this.wasDisabled
= null;
1734 this.$element
.addClass( 'oo-ui-widget' );
1735 this.setDisabled( !!config
.disabled
);
1740 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1741 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1748 * A 'disable' event is emitted when the disabled state of the widget changes
1749 * (i.e. on disable **and** enable).
1751 * @param {boolean} disabled Widget is disabled
1757 * A 'toggle' event is emitted when the visibility of the widget changes.
1759 * @param {boolean} visible Widget is visible
1765 * Check if the widget is disabled.
1767 * @return {boolean} Widget is disabled
1769 OO
.ui
.Widget
.prototype.isDisabled = function () {
1770 return this.disabled
;
1774 * Set the 'disabled' state of the widget.
1776 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1778 * @param {boolean} disabled Disable widget
1780 * @return {OO.ui.Widget} The widget, for chaining
1782 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1785 this.disabled
= !!disabled
;
1786 isDisabled
= this.isDisabled();
1787 if ( isDisabled
!== this.wasDisabled
) {
1788 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1789 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1790 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1791 this.emit( 'disable', isDisabled
);
1792 this.updateThemeClasses();
1794 this.wasDisabled
= isDisabled
;
1800 * Update the disabled state, in case of changes in parent widget.
1803 * @return {OO.ui.Widget} The widget, for chaining
1805 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1806 this.setDisabled( this.disabled
);
1811 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1814 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1817 * @return {string|null} The ID of the labelable element
1819 OO
.ui
.Widget
.prototype.getInputId = function () {
1824 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1825 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1826 * override this method to provide intuitive, accessible behavior.
1828 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1829 * Individual widgets may override it too.
1831 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1834 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1845 OO
.ui
.Theme
= function OoUiTheme() {
1846 this.elementClassesQueue
= [];
1847 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1852 OO
.initClass( OO
.ui
.Theme
);
1857 * Get a list of classes to be applied to a widget.
1859 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1860 * otherwise state transitions will not work properly.
1862 * @param {OO.ui.Element} element Element for which to get classes
1863 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1865 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1866 return { on
: [], off
: [] };
1870 * Update CSS classes provided by the theme.
1872 * For elements with theme logic hooks, this should be called any time there's a state change.
1874 * @param {OO.ui.Element} element Element for which to update classes
1876 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1877 var $elements
= $( [] ),
1878 classes
= this.getElementClasses( element
);
1880 if ( element
.$icon
) {
1881 $elements
= $elements
.add( element
.$icon
);
1883 if ( element
.$indicator
) {
1884 $elements
= $elements
.add( element
.$indicator
);
1888 .removeClass( classes
.off
)
1889 .addClass( classes
.on
);
1895 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1897 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1898 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1901 this.elementClassesQueue
= [];
1905 * Queue #updateElementClasses to be called for this element.
1907 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1908 * to make them synchronous.
1910 * @param {OO.ui.Element} element Element for which to update classes
1912 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1913 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1914 // the most common case (this method is often called repeatedly for the same element).
1915 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1918 this.elementClassesQueue
.push( element
);
1919 this.debouncedUpdateQueuedElementClasses();
1923 * Get the transition duration in milliseconds for dialogs opening/closing
1925 * The dialog should be fully rendered this many milliseconds after the
1926 * ready process has executed.
1928 * @return {number} Transition duration in milliseconds
1930 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1935 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1936 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1937 * order in which users will navigate through the focusable elements via the Tab key.
1940 * // TabIndexedElement is mixed into the ButtonWidget class
1941 * // to provide a tabIndex property.
1942 * var button1 = new OO.ui.ButtonWidget( {
1946 * button2 = new OO.ui.ButtonWidget( {
1950 * button3 = new OO.ui.ButtonWidget( {
1954 * button4 = new OO.ui.ButtonWidget( {
1958 * $( document.body ).append(
1969 * @param {Object} [config] Configuration options
1970 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1971 * the functionality is applied to the element created by the class ($element). If a different
1972 * element is specified, the tabindex functionality will be applied to it instead.
1973 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the
1974 * tab-navigation order (e.g., 1 for the first focusable element). Use 0 to use the default
1975 * navigation order; use -1 to remove the element from the tab-navigation flow.
1977 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1978 // Configuration initialization
1979 config
= $.extend( { tabIndex
: 0 }, config
);
1982 this.$tabIndexed
= null;
1983 this.tabIndex
= null;
1986 this.connect( this, {
1987 disable
: 'onTabIndexedElementDisable'
1991 this.setTabIndex( config
.tabIndex
);
1992 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1997 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
2002 * Set the element that should use the tabindex functionality.
2004 * This method is used to retarget a tabindex mixin so that its functionality applies
2005 * to the specified element. If an element is currently using the functionality, the mixin’s
2006 * effect on that element is removed before the new element is set up.
2008 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
2010 * @return {OO.ui.Element} The element, for chaining
2012 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
2013 var tabIndex
= this.tabIndex
;
2014 // Remove attributes from old $tabIndexed
2015 this.setTabIndex( null );
2016 // Force update of new $tabIndexed
2017 this.$tabIndexed
= $tabIndexed
;
2018 this.tabIndex
= tabIndex
;
2019 return this.updateTabIndex();
2023 * Set the value of the tabindex.
2025 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
2027 * @return {OO.ui.Element} The element, for chaining
2029 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
2030 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
2032 if ( this.tabIndex
!== tabIndex
) {
2033 this.tabIndex
= tabIndex
;
2034 this.updateTabIndex();
2041 * Update the `tabindex` attribute, in case of changes to tab index or
2046 * @return {OO.ui.Element} The element, for chaining
2048 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
2049 if ( this.$tabIndexed
) {
2050 if ( this.tabIndex
!== null ) {
2051 // Do not index over disabled elements
2052 this.$tabIndexed
.attr( {
2053 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
2054 // Support: ChromeVox and NVDA
2055 // These do not seem to inherit aria-disabled from parent elements
2056 'aria-disabled': this.isDisabled().toString()
2059 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
2066 * Handle disable events.
2069 * @param {boolean} disabled Element is disabled
2071 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
2072 this.updateTabIndex();
2076 * Get the value of the tabindex.
2078 * @return {number|null} Tabindex value
2080 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2081 return this.tabIndex
;
2085 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2087 * If the element already has an ID then that is returned, otherwise unique ID is
2088 * generated, set on the element, and returned.
2090 * @return {string|null} The ID of the focusable element
2092 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2095 if ( !this.$tabIndexed
) {
2098 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2102 id
= this.$tabIndexed
.attr( 'id' );
2103 if ( id
=== undefined ) {
2104 id
= OO
.ui
.generateElementId();
2105 this.$tabIndexed
.attr( 'id', id
);
2112 * Whether the node is 'labelable' according to the HTML spec
2113 * (i.e., whether it can be interacted with through a `<label for="…">`).
2114 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2117 * @param {jQuery} $node
2120 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2122 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2123 tagName
= ( $node
.prop( 'tagName' ) || '' ).toLowerCase();
2125 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2128 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2135 * Focus this element.
2138 * @return {OO.ui.Element} The element, for chaining
2140 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2141 if ( !this.isDisabled() ) {
2142 this.$tabIndexed
.trigger( 'focus' );
2148 * Blur this element.
2151 * @return {OO.ui.Element} The element, for chaining
2153 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2154 this.$tabIndexed
.trigger( 'blur' );
2159 * @inheritdoc OO.ui.Widget
2161 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2166 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2167 * interface element that can be configured with access keys for keyboard interaction.
2168 * See the [OOUI documentation on MediaWiki] [1] for examples.
2170 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2176 * @param {Object} [config] Configuration options
2177 * @cfg {jQuery} [$button] The button element created by the class.
2178 * If this configuration is omitted, the button element will use a generated `<a>`.
2179 * @cfg {boolean} [framed=true] Render the button with a frame
2181 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2182 // Configuration initialization
2183 config
= config
|| {};
2186 this.$button
= null;
2188 this.active
= config
.active
!== undefined && config
.active
;
2189 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
2190 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2191 this.onDocumentKeyUpHandler
= this.onDocumentKeyUp
.bind( this );
2192 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2193 this.onClickHandler
= this.onClick
.bind( this );
2194 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2197 this.$element
.addClass( 'oo-ui-buttonElement' );
2198 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2199 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2204 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2206 /* Static Properties */
2209 * Cancel mouse down events.
2211 * This property is usually set to `true` to prevent the focus from changing when the button is
2213 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and
2214 * {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} use a value of `false` so that dragging
2215 * behavior is possible and mousedown events can be handled by a parent widget.
2219 * @property {boolean}
2221 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2226 * A 'click' event is emitted when the button element is clicked.
2234 * Set the button element.
2236 * This method is used to retarget a button mixin so that its functionality applies to
2237 * the specified button element instead of the one created by the class. If a button element
2238 * is already set, the method will remove the mixin’s effect on that element.
2240 * @param {jQuery} $button Element to use as button
2242 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2243 if ( this.$button
) {
2245 .removeClass( 'oo-ui-buttonElement-button' )
2246 .removeAttr( 'role accesskey' )
2248 mousedown
: this.onMouseDownHandler
,
2249 keydown
: this.onKeyDownHandler
,
2250 click
: this.onClickHandler
,
2251 keypress
: this.onKeyPressHandler
2255 this.$button
= $button
2256 .addClass( 'oo-ui-buttonElement-button' )
2258 mousedown
: this.onMouseDownHandler
,
2259 keydown
: this.onKeyDownHandler
,
2260 click
: this.onClickHandler
,
2261 keypress
: this.onKeyPressHandler
2264 // Add `role="button"` on `<a>` elements, where it's needed
2265 // `toUpperCase()` is added for XHTML documents
2266 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2267 this.$button
.attr( 'role', 'button' );
2272 * Handles mouse down events.
2275 * @param {jQuery.Event} e Mouse down event
2276 * @return {undefined/boolean} False to prevent default if event is handled
2278 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2279 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2282 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2283 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2284 // reliably remove the pressed class
2285 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2286 // Prevent change of focus unless specifically configured otherwise
2287 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2293 * Handles document mouse up events.
2296 * @param {MouseEvent} e Mouse up event
2298 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentMouseUp = function ( e
) {
2299 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2302 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2303 // Stop listening for mouseup, since we only needed this once
2304 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2308 * Handles mouse click events.
2311 * @param {jQuery.Event} e Mouse click event
2313 * @return {undefined/boolean} False to prevent default if event is handled
2315 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2316 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2317 if ( this.emit( 'click' ) ) {
2324 * Handles key down events.
2327 * @param {jQuery.Event} e Key down event
2329 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2330 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2333 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2334 // Run the keyup handler no matter where the key is when the button is let go, so we can
2335 // reliably remove the pressed class
2336 this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2340 * Handles document key up events.
2343 * @param {KeyboardEvent} e Key up event
2345 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentKeyUp = function ( e
) {
2346 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2349 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2350 // Stop listening for keyup, since we only needed this once
2351 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2355 * Handles key press events.
2358 * @param {jQuery.Event} e Key press event
2360 * @return {undefined/boolean} False to prevent default if event is handled
2362 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2363 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2364 if ( this.emit( 'click' ) ) {
2371 * Check if button has a frame.
2373 * @return {boolean} Button is framed
2375 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2380 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame
2383 * @param {boolean} [framed] Make button framed, omit to toggle
2385 * @return {OO.ui.Element} The element, for chaining
2387 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2388 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2389 if ( framed
!== this.framed
) {
2390 this.framed
= framed
;
2392 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2393 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2394 this.updateThemeClasses();
2401 * Set the button's active state.
2403 * The active state can be set on:
2405 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2406 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2407 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2410 * @param {boolean} value Make button active
2412 * @return {OO.ui.Element} The element, for chaining
2414 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2415 this.active
= !!value
;
2416 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2417 this.updateThemeClasses();
2422 * Check if the button is active
2425 * @return {boolean} The button is active
2427 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2432 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2433 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2434 * items from the group is done through the interface the class provides.
2435 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2437 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2440 * @mixins OO.EmitterList
2444 * @param {Object} [config] Configuration options
2445 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2446 * is omitted, the group element will use a generated `<div>`.
2448 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2449 // Configuration initialization
2450 config
= config
|| {};
2452 // Mixin constructors
2453 OO
.EmitterList
.call( this, config
);
2459 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2464 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2471 * A change event is emitted when the set of selected items changes.
2473 * @param {OO.ui.Element[]} items Items currently in the group
2479 * Set the group element.
2481 * If an element is already set, items will be moved to the new element.
2483 * @param {jQuery} $group Element to use as group
2485 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2488 this.$group
= $group
;
2489 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2490 this.$group
.append( this.items
[ i
].$element
);
2495 * Find an item by its data.
2497 * Only the first item with matching data will be returned. To return all matching items,
2498 * use the #findItemsFromData method.
2500 * @param {Object} data Item data to search for
2501 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2503 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2505 hash
= OO
.getHash( data
);
2507 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2508 item
= this.items
[ i
];
2509 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2518 * Find items by their data.
2520 * All items with matching data will be returned. To return only the first match, use the
2521 * #findItemFromData method instead.
2523 * @param {Object} data Item data to search for
2524 * @return {OO.ui.Element[]} Items with equivalent data
2526 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2528 hash
= OO
.getHash( data
),
2531 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2532 item
= this.items
[ i
];
2533 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2542 * Add items to the group.
2544 * Items will be added to the end of the group array unless the optional `index` parameter
2545 * specifies a different insertion point. Adding an existing item will move it to the end of the
2546 * array or the point specified by the `index`.
2548 * @param {OO.ui.Element[]} items An array of items to add to the group
2549 * @param {number} [index] Index of the insertion point
2551 * @return {OO.ui.Element} The element, for chaining
2553 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2555 if ( items
.length
=== 0 ) {
2560 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2562 this.emit( 'change', this.getItems() );
2569 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2570 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2571 this.insertItemElements( items
, newIndex
);
2574 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2582 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2583 item
.setElementGroup( this );
2584 this.insertItemElements( item
, index
);
2587 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2593 * Insert elements into the group
2596 * @param {OO.ui.Element} itemWidget Item to insert
2597 * @param {number} index Insertion index
2599 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2600 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2601 this.$group
.append( itemWidget
.$element
);
2602 } else if ( index
=== 0 ) {
2603 this.$group
.prepend( itemWidget
.$element
);
2605 this.items
[ index
].$element
.before( itemWidget
.$element
);
2610 * Remove the specified items from a group.
2612 * Removed items are detached (not removed) from the DOM so that they may be reused.
2613 * To remove all items from a group, you may wish to use the #clearItems method instead.
2615 * @param {OO.ui.Element[]} items An array of items to remove
2617 * @return {OO.ui.Element} The element, for chaining
2619 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2620 var i
, len
, item
, index
;
2622 if ( items
.length
=== 0 ) {
2626 // Remove specific items elements
2627 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2629 index
= this.items
.indexOf( item
);
2630 if ( index
!== -1 ) {
2631 item
.setElementGroup( null );
2632 item
.$element
.detach();
2637 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2639 this.emit( 'change', this.getItems() );
2644 * Clear all items from the group.
2646 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2647 * To remove only a subset of items from a group, use the #removeItems method.
2650 * @return {OO.ui.Element} The element, for chaining
2652 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2655 // Remove all item elements
2656 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2657 this.items
[ i
].setElementGroup( null );
2658 this.items
[ i
].$element
.detach();
2662 OO
.EmitterList
.prototype.clearItems
.call( this );
2664 this.emit( 'change', this.getItems() );
2669 * LabelElement is often mixed into other classes to generate a label, which
2670 * helps identify the function of an interface element.
2671 * See the [OOUI documentation on MediaWiki] [1] for more information.
2673 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2679 * @param {Object} [config] Configuration options
2680 * @cfg {jQuery} [$label] The label element created by the class. If this
2681 * configuration is omitted, the label element will use a generated `<span>`.
2682 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be
2683 * specified as a plaintext string, a jQuery selection of elements, or a function that will
2684 * produce a string in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2685 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2686 * @cfg {boolean} [invisibleLabel] Whether the label should be visually hidden (but still
2687 * accessible to screen-readers).
2689 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2690 // Configuration initialization
2691 config
= config
|| {};
2696 this.invisibleLabel
= null;
2699 this.setLabel( config
.label
|| this.constructor.static.label
);
2700 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2701 this.setInvisibleLabel( config
.invisibleLabel
);
2706 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2711 * @event labelChange
2712 * @param {string} value
2715 /* Static Properties */
2718 * The label text. The label can be specified as a plaintext string, a function that will
2719 * produce a string in the future, or `null` for no label. The static value will
2720 * be overridden if a label is specified with the #label config option.
2724 * @property {string|Function|null}
2726 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2728 /* Static methods */
2731 * Highlight the first occurrence of the query in the given text
2733 * @param {string} text Text
2734 * @param {string} query Query to find
2735 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2736 * @return {jQuery} Text with the first match of the query
2737 * sub-string wrapped in highlighted span
2739 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2742 $result
= $( '<span>' );
2746 qLen
= query
.length
;
2747 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2748 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2753 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2756 if ( !query
.length
|| offset
=== -1 ) {
2757 $result
.text( text
);
2760 document
.createTextNode( text
.slice( 0, offset
) ),
2762 .addClass( 'oo-ui-labelElement-label-highlight' )
2763 .text( text
.slice( offset
, offset
+ query
.length
) ),
2764 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2767 return $result
.contents();
2773 * Set the label element.
2775 * If an element is already set, it will be cleaned up before setting up the new element.
2777 * @param {jQuery} $label Element to use as label
2779 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2780 if ( this.$label
) {
2781 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2784 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2785 this.setLabelContent( this.label
);
2791 * An empty string will result in the label being hidden. A string containing only whitespace will
2792 * be converted to a single ` `.
2794 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that
2795 * returns nodes or text; or null for no label
2797 * @return {OO.ui.Element} The element, for chaining
2799 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2800 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2801 label
= ( ( typeof label
=== 'string' || label
instanceof $ ) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2803 if ( this.label
!== label
) {
2804 if ( this.$label
) {
2805 this.setLabelContent( label
);
2808 this.emit( 'labelChange' );
2811 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2817 * Set whether the label should be visually hidden (but still accessible to screen-readers).
2819 * @param {boolean} invisibleLabel
2821 * @return {OO.ui.Element} The element, for chaining
2823 OO
.ui
.mixin
.LabelElement
.prototype.setInvisibleLabel = function ( invisibleLabel
) {
2824 invisibleLabel
= !!invisibleLabel
;
2826 if ( this.invisibleLabel
!== invisibleLabel
) {
2827 this.invisibleLabel
= invisibleLabel
;
2828 this.emit( 'labelChange' );
2831 this.$label
.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel
);
2832 // Pretend that there is no label, a lot of CSS has been written with this assumption
2833 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2839 * Set the label as plain text with a highlighted query
2841 * @param {string} text Text label to set
2842 * @param {string} query Substring of text to highlight
2843 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2845 * @return {OO.ui.Element} The element, for chaining
2847 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
2848 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
2854 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2855 * text; or null for no label
2857 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2862 * Set the content of the label.
2864 * Do not call this method until after the label element has been set by #setLabelElement.
2867 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2868 * text; or null for no label
2870 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2871 if ( typeof label
=== 'string' ) {
2872 if ( label
.match( /^\s*$/ ) ) {
2873 // Convert whitespace only string to a single non-breaking space
2874 this.$label
.html( ' ' );
2876 this.$label
.text( label
);
2878 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2879 this.$label
.html( label
.toString() );
2880 } else if ( label
instanceof $ ) {
2881 this.$label
.empty().append( label
);
2883 this.$label
.empty();
2888 * IconElement is often mixed into other classes to generate an icon.
2889 * Icons are graphics, about the size of normal text. They are used to aid the user
2890 * in locating a control or to convey information in a space-efficient way. See the
2891 * [OOUI documentation on MediaWiki] [1] for a list of icons
2892 * included in the library.
2894 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2900 * @param {Object} [config] Configuration options
2901 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2902 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2903 * the icon element be set to an existing icon instead of the one generated by this class, set a
2904 * value using a jQuery selection. For example:
2906 * // Use a <div> tag instead of a <span>
2907 * $icon: $( '<div>' )
2908 * // Use an existing icon element instead of the one generated by the class
2909 * $icon: this.$element
2910 * // Use an icon element from a child widget
2911 * $icon: this.childwidget.$element
2912 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a
2913 * map of symbolic names. A map is used for i18n purposes and contains a `default` icon
2914 * name and additional names keyed by language code. The `default` name is used when no icon is
2915 * keyed by the user's language.
2917 * Example of an i18n map:
2919 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2920 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2921 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2923 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2924 // Configuration initialization
2925 config
= config
|| {};
2932 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2933 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2938 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2940 /* Static Properties */
2943 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map
2944 * is used for i18n purposes and contains a `default` icon name and additional names keyed by
2945 * language code. The `default` name is used when no icon is keyed by the user's language.
2947 * Example of an i18n map:
2949 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2951 * Note: the static property will be overridden if the #icon configuration is used.
2955 * @property {Object|string}
2957 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2960 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2961 * function that returns title text, or `null` for no title.
2963 * The static property will be overridden if the #iconTitle configuration is used.
2967 * @property {string|Function|null}
2969 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2974 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2975 * applies to the specified icon element instead of the one created by the class. If an icon
2976 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2977 * and mixin methods will no longer affect the element.
2979 * @param {jQuery} $icon Element to use as icon
2981 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2984 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2985 .removeAttr( 'title' );
2989 .addClass( 'oo-ui-iconElement-icon' )
2990 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
2991 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2992 if ( this.iconTitle
!== null ) {
2993 this.$icon
.attr( 'title', this.iconTitle
);
2996 this.updateThemeClasses();
3000 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
3001 * The icon parameter can also be set to a map of icon names. See the #icon config setting
3004 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
3005 * by language code, or `null` to remove the icon.
3007 * @return {OO.ui.Element} The element, for chaining
3009 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
3010 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
3011 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
3013 if ( this.icon
!== icon
) {
3015 if ( this.icon
!== null ) {
3016 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
3018 if ( icon
!== null ) {
3019 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
3025 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
3027 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
3029 this.updateThemeClasses();
3035 * Get the symbolic name of the icon.
3037 * @return {string} Icon name
3039 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
3044 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
3046 * @return {string} Icon title text
3049 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
3050 return this.iconTitle
;
3054 * IndicatorElement is often mixed into other classes to generate an indicator.
3055 * Indicators are small graphics that are generally used in two ways:
3057 * - To draw attention to the status of an item. For example, an indicator might be
3058 * used to show that an item in a list has errors that need to be resolved.
3059 * - To clarify the function of a control that acts in an exceptional way (a button
3060 * that opens a menu instead of performing an action directly, for example).
3062 * For a list of indicators included in the library, please see the
3063 * [OOUI documentation on MediaWiki] [1].
3065 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3071 * @param {Object} [config] Configuration options
3072 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
3073 * configuration is omitted, the indicator element will use a generated `<span>`.
3074 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3075 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3077 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3079 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
3080 // Configuration initialization
3081 config
= config
|| {};
3084 this.$indicator
= null;
3085 this.indicator
= null;
3088 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
3089 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
3094 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
3096 /* Static Properties */
3099 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3100 * The static property will be overridden if the #indicator configuration is used.
3104 * @property {string|null}
3106 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
3109 * A text string used as the indicator title, a function that returns title text, or `null`
3110 * for no title. The static property will be overridden if the #indicatorTitle configuration is
3115 * @property {string|Function|null}
3117 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
3122 * Set the indicator element.
3124 * If an element is already set, it will be cleaned up before setting up the new element.
3126 * @param {jQuery} $indicator Element to use as indicator
3128 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
3129 if ( this.$indicator
) {
3131 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
3132 .removeAttr( 'title' );
3135 this.$indicator
= $indicator
3136 .addClass( 'oo-ui-indicatorElement-indicator' )
3137 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
3138 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
3139 if ( this.indicatorTitle
!== null ) {
3140 this.$indicator
.attr( 'title', this.indicatorTitle
);
3143 this.updateThemeClasses();
3147 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null`
3148 * to remove the indicator.
3150 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3152 * @return {OO.ui.Element} The element, for chaining
3154 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
3155 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
3157 if ( this.indicator
!== indicator
) {
3158 if ( this.$indicator
) {
3159 if ( this.indicator
!== null ) {
3160 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
3162 if ( indicator
!== null ) {
3163 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
3166 this.indicator
= indicator
;
3169 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
3170 if ( this.$indicator
) {
3171 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
3173 this.updateThemeClasses();
3179 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3181 * @return {string} Symbolic name of indicator
3183 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
3184 return this.indicator
;
3188 * Get the indicator title.
3190 * The title is displayed when a user moves the mouse over the indicator.
3192 * @return {string} Indicator title text
3195 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
3196 return this.indicatorTitle
;
3200 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3201 * additional functionality to an element created by another class. The class provides
3202 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3203 * which are used to customize the look and feel of a widget to better describe its
3204 * importance and functionality.
3206 * The library currently contains the following styling flags for general use:
3208 * - **progressive**: Progressive styling is applied to convey that the widget will move the user
3209 * forward in a process.
3210 * - **destructive**: Destructive styling is applied to convey that the widget will remove
3213 * The flags affect the appearance of the buttons:
3216 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3217 * var button1 = new OO.ui.ButtonWidget( {
3218 * label: 'Progressive',
3219 * flags: 'progressive'
3221 * button2 = new OO.ui.ButtonWidget( {
3222 * label: 'Destructive',
3223 * flags: 'destructive'
3225 * $( document.body ).append( button1.$element, button2.$element );
3227 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an
3228 * action, use these flags: **primary** and **safe**.
3229 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3231 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3237 * @param {Object} [config] Configuration options
3238 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary')
3240 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3241 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3242 * @cfg {jQuery} [$flagged] The flagged element. By default,
3243 * the flagged functionality is applied to the element created by the class ($element).
3244 * If a different element is specified, the flagged functionality will be applied to it instead.
3246 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3247 // Configuration initialization
3248 config
= config
|| {};
3252 this.$flagged
= null;
3255 this.setFlags( config
.flags
);
3256 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3263 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3264 * parameter contains the name of each modified flag and indicates whether it was
3267 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3268 * that the flag was added, `false` that the flag was removed.
3274 * Set the flagged element.
3276 * This method is used to retarget a flagged mixin so that its functionality applies to the
3277 * specified element.
3278 * If an element is already set, the method will remove the mixin’s effect on that element.
3280 * @param {jQuery} $flagged Element that should be flagged
3282 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3283 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3284 return 'oo-ui-flaggedElement-' + flag
;
3287 if ( this.$flagged
) {
3288 this.$flagged
.removeClass( classNames
);
3291 this.$flagged
= $flagged
.addClass( classNames
);
3295 * Check if the specified flag is set.
3297 * @param {string} flag Name of flag
3298 * @return {boolean} The flag is set
3300 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3301 // This may be called before the constructor, thus before this.flags is set
3302 return this.flags
&& ( flag
in this.flags
);
3306 * Get the names of all flags set.
3308 * @return {string[]} Flag names
3310 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3311 // This may be called before the constructor, thus before this.flags is set
3312 return Object
.keys( this.flags
|| {} );
3319 * @return {OO.ui.Element} The element, for chaining
3322 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3323 var flag
, className
,
3326 classPrefix
= 'oo-ui-flaggedElement-';
3328 for ( flag
in this.flags
) {
3329 className
= classPrefix
+ flag
;
3330 changes
[ flag
] = false;
3331 delete this.flags
[ flag
];
3332 remove
.push( className
);
3335 if ( this.$flagged
) {
3336 this.$flagged
.removeClass( remove
);
3339 this.updateThemeClasses();
3340 this.emit( 'flag', changes
);
3346 * Add one or more flags.
3348 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3349 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3350 * be added (`true`) or removed (`false`).
3352 * @return {OO.ui.Element} The element, for chaining
3355 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3356 var i
, len
, flag
, className
,
3360 classPrefix
= 'oo-ui-flaggedElement-';
3362 if ( typeof flags
=== 'string' ) {
3363 className
= classPrefix
+ flags
;
3365 if ( !this.flags
[ flags
] ) {
3366 this.flags
[ flags
] = true;
3367 add
.push( className
);
3369 } else if ( Array
.isArray( flags
) ) {
3370 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3372 className
= classPrefix
+ flag
;
3374 if ( !this.flags
[ flag
] ) {
3375 changes
[ flag
] = true;
3376 this.flags
[ flag
] = true;
3377 add
.push( className
);
3380 } else if ( OO
.isPlainObject( flags
) ) {
3381 for ( flag
in flags
) {
3382 className
= classPrefix
+ flag
;
3383 if ( flags
[ flag
] ) {
3385 if ( !this.flags
[ flag
] ) {
3386 changes
[ flag
] = true;
3387 this.flags
[ flag
] = true;
3388 add
.push( className
);
3392 if ( this.flags
[ flag
] ) {
3393 changes
[ flag
] = false;
3394 delete this.flags
[ flag
];
3395 remove
.push( className
);
3401 if ( this.$flagged
) {
3404 .removeClass( remove
);
3407 this.updateThemeClasses();
3408 this.emit( 'flag', changes
);
3414 * TitledElement is mixed into other classes to provide a `title` attribute.
3415 * Titles are rendered by the browser and are made visible when the user moves
3416 * the mouse over the element. Titles are not visible on touch devices.
3419 * // TitledElement provides a `title` attribute to the
3420 * // ButtonWidget class.
3421 * var button = new OO.ui.ButtonWidget( {
3422 * label: 'Button with Title',
3423 * title: 'I am a button'
3425 * $( document.body ).append( button.$element );
3431 * @param {Object} [config] Configuration options
3432 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3433 * If this config is omitted, the title functionality is applied to $element, the
3434 * element created by the class.
3435 * @cfg {string|Function} [title] The title text or a function that returns text. If
3436 * this config is omitted, the value of the {@link #static-title static title} property is used.
3438 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3439 // Configuration initialization
3440 config
= config
|| {};
3443 this.$titled
= null;
3447 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3448 this.setTitledElement( config
.$titled
|| this.$element
);
3453 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3455 /* Static Properties */
3458 * The title text, a function that returns text, or `null` for no title. The value of the static
3459 * property is overridden if the #title config option is used.
3463 * @property {string|Function|null}
3465 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3470 * Set the titled element.
3472 * This method is used to retarget a TitledElement mixin so that its functionality applies to the
3473 * specified element.
3474 * If an element is already set, the mixin’s effect on that element is removed before the new
3475 * element is set up.
3477 * @param {jQuery} $titled Element that should use the 'titled' functionality
3479 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3480 if ( this.$titled
) {
3481 this.$titled
.removeAttr( 'title' );
3484 this.$titled
= $titled
;
3493 * @param {string|Function|null} title Title text, a function that returns text, or `null`
3496 * @return {OO.ui.Element} The element, for chaining
3498 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3499 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3500 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3502 if ( this.title
!== title
) {
3511 * Update the title attribute, in case of changes to title or accessKey.
3515 * @return {OO.ui.Element} The element, for chaining
3517 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3518 var title
= this.getTitle();
3519 if ( this.$titled
) {
3520 if ( title
!== null ) {
3521 // Only if this is an AccessKeyedElement
3522 if ( this.formatTitleWithAccessKey
) {
3523 title
= this.formatTitleWithAccessKey( title
);
3525 this.$titled
.attr( 'title', title
);
3527 this.$titled
.removeAttr( 'title' );
3536 * @return {string} Title string
3538 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3543 * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
3544 * Access keys allow an user to go to a specific element by using
3545 * a shortcut combination of a browser specific keys + the key
3549 * // AccessKeyedElement provides an `accesskey` attribute to the
3550 * // ButtonWidget class.
3551 * var button = new OO.ui.ButtonWidget( {
3552 * label: 'Button with access key',
3555 * $( document.body ).append( button.$element );
3561 * @param {Object} [config] Configuration options
3562 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3563 * If this config is omitted, the access key functionality is applied to $element, the
3564 * element created by the class.
3565 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3566 * this config is omitted, no access key will be added.
3568 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3569 // Configuration initialization
3570 config
= config
|| {};
3573 this.$accessKeyed
= null;
3574 this.accessKey
= null;
3577 this.setAccessKey( config
.accessKey
|| null );
3578 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3580 // If this is also a TitledElement and it initialized before we did, we may have
3581 // to update the title with the access key
3582 if ( this.updateTitle
) {
3589 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3591 /* Static Properties */
3594 * The access key, a function that returns a key, or `null` for no access key.
3598 * @property {string|Function|null}
3600 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3605 * Set the access keyed element.
3607 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to
3608 * the specified element.
3609 * If an element is already set, the mixin's effect on that element is removed before the new
3610 * element is set up.
3612 * @param {jQuery} $accessKeyed Element that should use the 'access keyed' functionality
3614 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3615 if ( this.$accessKeyed
) {
3616 this.$accessKeyed
.removeAttr( 'accesskey' );
3619 this.$accessKeyed
= $accessKeyed
;
3620 if ( this.accessKey
) {
3621 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3628 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no
3631 * @return {OO.ui.Element} The element, for chaining
3633 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3634 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3636 if ( this.accessKey
!== accessKey
) {
3637 if ( this.$accessKeyed
) {
3638 if ( accessKey
!== null ) {
3639 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3641 this.$accessKeyed
.removeAttr( 'accesskey' );
3644 this.accessKey
= accessKey
;
3646 // Only if this is a TitledElement
3647 if ( this.updateTitle
) {
3658 * @return {string} accessKey string
3660 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3661 return this.accessKey
;
3665 * Add information about the access key to the element's tooltip label.
3666 * (This is only public for hacky usage in FieldLayout.)
3668 * @param {string} title Tooltip label for `title` attribute
3671 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3674 if ( !this.$accessKeyed
) {
3675 // Not initialized yet; the constructor will call updateTitle() which will rerun this
3679 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the
3681 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3682 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3684 accessKey
= this.getAccessKey();
3687 title
+= ' [' + accessKey
+ ']';
3693 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3694 * feels, and functionality can be customized via the class’s configuration options
3695 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3698 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3701 * // A button widget.
3702 * var button = new OO.ui.ButtonWidget( {
3703 * label: 'Button with Icon',
3707 * $( document.body ).append( button.$element );
3709 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3712 * @extends OO.ui.Widget
3713 * @mixins OO.ui.mixin.ButtonElement
3714 * @mixins OO.ui.mixin.IconElement
3715 * @mixins OO.ui.mixin.IndicatorElement
3716 * @mixins OO.ui.mixin.LabelElement
3717 * @mixins OO.ui.mixin.TitledElement
3718 * @mixins OO.ui.mixin.FlaggedElement
3719 * @mixins OO.ui.mixin.TabIndexedElement
3720 * @mixins OO.ui.mixin.AccessKeyedElement
3723 * @param {Object} [config] Configuration options
3724 * @cfg {boolean} [active=false] Whether button should be shown as active
3725 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3726 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3727 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3729 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3730 // Configuration initialization
3731 config
= config
|| {};
3733 // Parent constructor
3734 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3736 // Mixin constructors
3737 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3738 OO
.ui
.mixin
.IconElement
.call( this, config
);
3739 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3740 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3741 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
3742 $titled
: this.$button
3744 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3745 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
3746 $tabIndexed
: this.$button
3748 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
3749 $accessKeyed
: this.$button
3755 this.noFollow
= false;
3758 this.connect( this, {
3759 disable
: 'onDisable'
3763 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3765 .addClass( 'oo-ui-buttonWidget' )
3766 .append( this.$button
);
3767 this.setActive( config
.active
);
3768 this.setHref( config
.href
);
3769 this.setTarget( config
.target
);
3770 this.setNoFollow( config
.noFollow
);
3775 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3776 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3777 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3778 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3779 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3780 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3781 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3782 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3783 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3785 /* Static Properties */
3791 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3797 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3802 * Get hyperlink location.
3804 * @return {string} Hyperlink location
3806 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3811 * Get hyperlink target.
3813 * @return {string} Hyperlink target
3815 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3820 * Get search engine traversal hint.
3822 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3824 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3825 return this.noFollow
;
3829 * Set hyperlink location.
3831 * @param {string|null} href Hyperlink location, null to remove
3833 * @return {OO.ui.Widget} The widget, for chaining
3835 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3836 href
= typeof href
=== 'string' ? href
: null;
3837 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3841 if ( href
!== this.href
) {
3850 * Update the `href` attribute, in case of changes to href or
3855 * @return {OO.ui.Widget} The widget, for chaining
3857 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3858 if ( this.href
!== null && !this.isDisabled() ) {
3859 this.$button
.attr( 'href', this.href
);
3861 this.$button
.removeAttr( 'href' );
3868 * Handle disable events.
3871 * @param {boolean} disabled Element is disabled
3873 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3878 * Set hyperlink target.
3880 * @param {string|null} target Hyperlink target, null to remove
3881 * @return {OO.ui.Widget} The widget, for chaining
3883 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3884 target
= typeof target
=== 'string' ? target
: null;
3886 if ( target
!== this.target
) {
3887 this.target
= target
;
3888 if ( target
!== null ) {
3889 this.$button
.attr( 'target', target
);
3891 this.$button
.removeAttr( 'target' );
3899 * Set search engine traversal hint.
3901 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3902 * @return {OO.ui.Widget} The widget, for chaining
3904 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3905 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3907 if ( noFollow
!== this.noFollow
) {
3908 this.noFollow
= noFollow
;
3910 this.$button
.attr( 'rel', 'nofollow' );
3912 this.$button
.removeAttr( 'rel' );
3919 // Override method visibility hints from ButtonElement
3930 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3931 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3932 * removed, and cleared from the group.
3935 * // A ButtonGroupWidget with two buttons.
3936 * var button1 = new OO.ui.PopupButtonWidget( {
3937 * label: 'Select a category',
3940 * $content: $( '<p>List of categories…</p>' ),
3945 * button2 = new OO.ui.ButtonWidget( {
3948 * buttonGroup = new OO.ui.ButtonGroupWidget( {
3949 * items: [ button1, button2 ]
3951 * $( document.body ).append( buttonGroup.$element );
3954 * @extends OO.ui.Widget
3955 * @mixins OO.ui.mixin.GroupElement
3956 * @mixins OO.ui.mixin.TitledElement
3959 * @param {Object} [config] Configuration options
3960 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3962 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3963 // Configuration initialization
3964 config
= config
|| {};
3966 // Parent constructor
3967 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
3969 // Mixin constructors
3970 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {
3971 $group
: this.$element
3973 OO
.ui
.mixin
.TitledElement
.call( this, config
);
3976 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
3977 if ( Array
.isArray( config
.items
) ) {
3978 this.addItems( config
.items
);
3984 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
3985 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
3986 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.TitledElement
);
3988 /* Static Properties */
3994 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
4002 * @return {OO.ui.Widget} The widget, for chaining
4004 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
4005 if ( !this.isDisabled() ) {
4006 if ( this.items
[ 0 ] ) {
4007 this.items
[ 0 ].focus();
4016 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
4021 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}.
4022 * In general, IconWidgets should be used with OO.ui.LabelWidget, which creates a label that
4023 * identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
4024 * for a list of icons included in the library.
4027 * // An IconWidget with a label via LabelWidget.
4028 * var myIcon = new OO.ui.IconWidget( {
4032 * // Create a label.
4033 * iconLabel = new OO.ui.LabelWidget( {
4036 * $( document.body ).append( myIcon.$element, iconLabel.$element );
4038 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4041 * @extends OO.ui.Widget
4042 * @mixins OO.ui.mixin.IconElement
4043 * @mixins OO.ui.mixin.TitledElement
4044 * @mixins OO.ui.mixin.LabelElement
4045 * @mixins OO.ui.mixin.FlaggedElement
4048 * @param {Object} [config] Configuration options
4050 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
4051 // Configuration initialization
4052 config
= config
|| {};
4054 // Parent constructor
4055 OO
.ui
.IconWidget
.parent
.call( this, config
);
4057 // Mixin constructors
4058 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {
4059 $icon
: this.$element
4061 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4062 $titled
: this.$element
4064 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4065 $label
: this.$element
,
4066 invisibleLabel
: true
4068 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {
4069 $flagged
: this.$element
4073 this.$element
.addClass( 'oo-ui-iconWidget' );
4074 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4075 // nested in other widgets, because this widget used to not mix in LabelElement.
4076 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4081 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
4082 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
4083 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
4084 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.LabelElement
);
4085 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
4087 /* Static Properties */
4093 OO
.ui
.IconWidget
.static.tagName
= 'span';
4096 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4097 * attention to the status of an item or to clarify the function within a control. For a list of
4098 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4101 * // An indicator widget.
4102 * var indicator1 = new OO.ui.IndicatorWidget( {
4103 * indicator: 'required'
4105 * // Create a fieldset layout to add a label.
4106 * fieldset = new OO.ui.FieldsetLayout();
4107 * fieldset.addItems( [
4108 * new OO.ui.FieldLayout( indicator1, {
4109 * label: 'A required indicator:'
4112 * $( document.body ).append( fieldset.$element );
4114 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4117 * @extends OO.ui.Widget
4118 * @mixins OO.ui.mixin.IndicatorElement
4119 * @mixins OO.ui.mixin.TitledElement
4120 * @mixins OO.ui.mixin.LabelElement
4123 * @param {Object} [config] Configuration options
4125 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4126 // Configuration initialization
4127 config
= config
|| {};
4129 // Parent constructor
4130 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4132 // Mixin constructors
4133 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {
4134 $indicator
: this.$element
4136 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
4137 $titled
: this.$element
4139 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4140 $label
: this.$element
,
4141 invisibleLabel
: true
4145 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4146 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4147 // nested in other widgets, because this widget used to not mix in LabelElement.
4148 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4153 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4154 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4155 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4156 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.LabelElement
);
4158 /* Static Properties */
4164 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4167 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4168 * be configured with a `label` option that is set to a string, a label node, or a function:
4170 * - String: a plaintext string
4171 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4172 * label that includes a link or special styling, such as a gray color or additional
4173 * graphical elements.
4174 * - Function: a function that will produce a string in the future. Functions are used
4175 * in cases where the value of the label is not currently defined.
4177 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget},
4178 * which will come into focus when the label is clicked.
4181 * // Two LabelWidgets.
4182 * var label1 = new OO.ui.LabelWidget( {
4183 * label: 'plaintext label'
4185 * label2 = new OO.ui.LabelWidget( {
4186 * label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4188 * // Create a fieldset layout with fields for each example.
4189 * fieldset = new OO.ui.FieldsetLayout();
4190 * fieldset.addItems( [
4191 * new OO.ui.FieldLayout( label1 ),
4192 * new OO.ui.FieldLayout( label2 )
4194 * $( document.body ).append( fieldset.$element );
4197 * @extends OO.ui.Widget
4198 * @mixins OO.ui.mixin.LabelElement
4199 * @mixins OO.ui.mixin.TitledElement
4202 * @param {Object} [config] Configuration options
4203 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4204 * Clicking the label will focus the specified input field.
4206 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4207 // Configuration initialization
4208 config
= config
|| {};
4210 // Parent constructor
4211 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4213 // Mixin constructors
4214 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
4215 $label
: this.$element
4217 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4220 this.input
= config
.input
;
4224 if ( this.input
.getInputId() ) {
4225 this.$element
.attr( 'for', this.input
.getInputId() );
4227 this.$label
.on( 'click', function () {
4228 this.input
.simulateLabelClick();
4232 this.$element
.addClass( 'oo-ui-labelWidget' );
4237 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4238 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4239 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4241 /* Static Properties */
4247 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4250 * PendingElement is a mixin that is used to create elements that notify users that something is
4251 * happening and that they should wait before proceeding. The pending state is visually represented
4252 * with a pending texture that appears in the head of a pending
4253 * {@link OO.ui.ProcessDialog process dialog} or in the input field of a
4254 * {@link OO.ui.TextInputWidget text input widget}.
4256 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked
4257 * as pending, but only when used in {@link OO.ui.MessageDialog message dialogs}. The behavior is
4258 * not currently supported for action widgets used in process dialogs.
4261 * function MessageDialog( config ) {
4262 * MessageDialog.parent.call( this, config );
4264 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4266 * MessageDialog.static.name = 'myMessageDialog';
4267 * MessageDialog.static.actions = [
4268 * { action: 'save', label: 'Done', flags: 'primary' },
4269 * { label: 'Cancel', flags: 'safe' }
4272 * MessageDialog.prototype.initialize = function () {
4273 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4274 * this.content = new OO.ui.PanelLayout( { padded: true } );
4275 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending ' +
4276 * 'state. Note that action widgets can be marked pending in message dialogs but not ' +
4277 * 'process dialogs.</p>' );
4278 * this.$body.append( this.content.$element );
4280 * MessageDialog.prototype.getBodyHeight = function () {
4283 * MessageDialog.prototype.getActionProcess = function ( action ) {
4284 * var dialog = this;
4285 * if ( action === 'save' ) {
4286 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4287 * return new OO.ui.Process()
4289 * .next( function () {
4290 * dialog.getActions().get({actions: 'save'})[0].popPending();
4293 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4296 * var windowManager = new OO.ui.WindowManager();
4297 * $( document.body ).append( windowManager.$element );
4299 * var dialog = new MessageDialog();
4300 * windowManager.addWindows( [ dialog ] );
4301 * windowManager.openWindow( dialog );
4307 * @param {Object} [config] Configuration options
4308 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4310 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4311 // Configuration initialization
4312 config
= config
|| {};
4316 this.$pending
= null;
4319 this.setPendingElement( config
.$pending
|| this.$element
);
4324 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4329 * Set the pending element (and clean up any existing one).
4331 * @param {jQuery} $pending The element to set to pending.
4333 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4334 if ( this.$pending
) {
4335 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4338 this.$pending
= $pending
;
4339 if ( this.pending
> 0 ) {
4340 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4345 * Check if an element is pending.
4347 * @return {boolean} Element is pending
4349 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4350 return !!this.pending
;
4354 * Increase the pending counter. The pending state will remain active until the counter is zero
4355 * (i.e., the number of calls to #pushPending and #popPending is the same).
4358 * @return {OO.ui.Element} The element, for chaining
4360 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4361 if ( this.pending
=== 0 ) {
4362 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4363 this.updateThemeClasses();
4371 * Decrease the pending counter. The pending state will remain active until the counter is zero
4372 * (i.e., the number of calls to #pushPending and #popPending is the same).
4375 * @return {OO.ui.Element} The element, for chaining
4377 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4378 if ( this.pending
=== 1 ) {
4379 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4380 this.updateThemeClasses();
4382 this.pending
= Math
.max( 0, this.pending
- 1 );
4388 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4389 * in the document (for example, in an OO.ui.Window's $overlay).
4391 * The elements's position is automatically calculated and maintained when window is resized or the
4392 * page is scrolled. If you reposition the container manually, you have to call #position to make
4393 * sure the element is still placed correctly.
4395 * As positioning is only possible when both the element and the container are attached to the DOM
4396 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4397 * the #toggle method to display a floating popup, for example.
4403 * @param {Object} [config] Configuration options
4404 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4405 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4406 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4407 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4408 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4409 * 'top': Align the top edge with $floatableContainer's top edge
4410 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4411 * 'center': Vertically align the center with $floatableContainer's center
4412 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4413 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4414 * 'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
4415 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4416 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4417 * 'center': Horizontally align the center with $floatableContainer's center
4418 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4421 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4422 // Configuration initialization
4423 config
= config
|| {};
4426 this.$floatable
= null;
4427 this.$floatableContainer
= null;
4428 this.$floatableWindow
= null;
4429 this.$floatableClosestScrollable
= null;
4430 this.floatableOutOfView
= false;
4431 this.onFloatableScrollHandler
= this.position
.bind( this );
4432 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4435 this.setFloatableContainer( config
.$floatableContainer
);
4436 this.setFloatableElement( config
.$floatable
|| this.$element
);
4437 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4438 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4439 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ?
4440 true : !!config
.hideWhenOutOfView
;
4446 * Set floatable element.
4448 * If an element is already set, it will be cleaned up before setting up the new element.
4450 * @param {jQuery} $floatable Element to make floatable
4452 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4453 if ( this.$floatable
) {
4454 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4455 this.$floatable
.css( { left
: '', top
: '' } );
4458 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4463 * Set floatable container.
4465 * The element will be positioned relative to the specified container.
4467 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4469 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4470 this.$floatableContainer
= $floatableContainer
;
4471 if ( this.$floatable
) {
4477 * Change how the element is positioned vertically.
4479 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4481 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4482 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4483 throw new Error( 'Invalid value for vertical position: ' + position
);
4485 if ( this.verticalPosition
!== position
) {
4486 this.verticalPosition
= position
;
4487 if ( this.$floatable
) {
4494 * Change how the element is positioned horizontally.
4496 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4498 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4499 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4500 throw new Error( 'Invalid value for horizontal position: ' + position
);
4502 if ( this.horizontalPosition
!== position
) {
4503 this.horizontalPosition
= position
;
4504 if ( this.$floatable
) {
4511 * Toggle positioning.
4513 * Do not turn positioning on until after the element is attached to the DOM and visible.
4515 * @param {boolean} [positioning] Enable positioning, omit to toggle
4517 * @return {OO.ui.Element} The element, for chaining
4519 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4520 var closestScrollableOfContainer
;
4522 if ( !this.$floatable
|| !this.$floatableContainer
) {
4526 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4528 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4529 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4530 this.warnedUnattached
= true;
4533 if ( this.positioning
!== positioning
) {
4534 this.positioning
= positioning
;
4536 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer(
4537 this.$floatableContainer
[ 0 ]
4539 // If the scrollable is the root, we have to listen to scroll events
4540 // on the window because of browser inconsistencies.
4541 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4542 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow(
4543 closestScrollableOfContainer
4547 if ( positioning
) {
4548 this.$floatableWindow
= $( this.getElementWindow() );
4549 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4551 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4552 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4554 // Initial position after visible
4557 if ( this.$floatableWindow
) {
4558 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4559 this.$floatableWindow
= null;
4562 if ( this.$floatableClosestScrollable
) {
4563 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4564 this.$floatableClosestScrollable
= null;
4567 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4575 * Check whether the bottom edge of the given element is within the viewport of the given
4579 * @param {jQuery} $element
4580 * @param {jQuery} $container
4583 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4584 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
,
4585 rightEdgeInBounds
, startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4586 direction
= $element
.css( 'direction' );
4588 elemRect
= $element
[ 0 ].getBoundingClientRect();
4589 if ( $container
[ 0 ] === window
) {
4590 viewportSpacing
= OO
.ui
.getViewportSpacing();
4594 right
: document
.documentElement
.clientWidth
,
4595 bottom
: document
.documentElement
.clientHeight
4597 contRect
.top
+= viewportSpacing
.top
;
4598 contRect
.left
+= viewportSpacing
.left
;
4599 contRect
.right
-= viewportSpacing
.right
;
4600 contRect
.bottom
-= viewportSpacing
.bottom
;
4602 contRect
= $container
[ 0 ].getBoundingClientRect();
4605 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4606 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4607 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4608 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4609 if ( direction
=== 'rtl' ) {
4610 startEdgeInBounds
= rightEdgeInBounds
;
4611 endEdgeInBounds
= leftEdgeInBounds
;
4613 startEdgeInBounds
= leftEdgeInBounds
;
4614 endEdgeInBounds
= rightEdgeInBounds
;
4617 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4620 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4623 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4626 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4630 // The other positioning values are all about being inside the container,
4631 // so in those cases all we care about is that any part of the container is visible.
4632 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4633 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4637 * Check if the floatable is hidden to the user because it was offscreen.
4639 * @return {boolean} Floatable is out of view
4641 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4642 return this.floatableOutOfView
;
4646 * Position the floatable below its container.
4648 * This should only be done when both of them are attached to the DOM and visible.
4651 * @return {OO.ui.Element} The element, for chaining
4653 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4654 if ( !this.positioning
) {
4659 // To continue, some things need to be true:
4660 // The element must actually be in the DOM
4661 this.isElementAttached() && (
4662 // The closest scrollable is the current window
4663 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4664 // OR is an element in the element's DOM
4665 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4668 // Abort early if important parts of the widget are no longer attached to the DOM
4672 this.floatableOutOfView
= this.hideWhenOutOfView
&&
4673 !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4674 if ( this.floatableOutOfView
) {
4675 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4678 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4681 this.$floatable
.css( this.computePosition() );
4683 // We updated the position, so re-evaluate the clipping state.
4684 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4685 // will not notice the need to update itself.)
4686 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here.
4687 // Why does it not listen to the right events in the right places?
4696 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4697 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4698 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4700 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4702 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4703 var isBody
, scrollableX
, scrollableY
, containerPos
,
4704 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4705 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4706 direction
= this.$floatableContainer
.css( 'direction' ),
4707 $offsetParent
= this.$floatable
.offsetParent();
4709 if ( $offsetParent
.is( 'html' ) ) {
4710 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4711 // <html> element, but they do work on the <body>
4712 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4714 isBody
= $offsetParent
.is( 'body' );
4715 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' ||
4716 $offsetParent
.css( 'overflow-x' ) === 'auto';
4717 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' ||
4718 $offsetParent
.css( 'overflow-y' ) === 'auto';
4720 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4721 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4722 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container
4723 // is the body, or if it isn't scrollable
4724 scrollTop
= scrollableY
&& !isBody
?
4725 $offsetParent
.scrollTop() : 0;
4726 scrollLeft
= scrollableX
&& !isBody
?
4727 OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4729 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4730 // if the <body> has a margin
4731 containerPos
= isBody
?
4732 this.$floatableContainer
.offset() :
4733 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4734 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4735 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4736 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4737 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4739 if ( this.verticalPosition
=== 'below' ) {
4740 newPos
.top
= containerPos
.bottom
;
4741 } else if ( this.verticalPosition
=== 'above' ) {
4742 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4743 } else if ( this.verticalPosition
=== 'top' ) {
4744 newPos
.top
= containerPos
.top
;
4745 } else if ( this.verticalPosition
=== 'bottom' ) {
4746 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4747 } else if ( this.verticalPosition
=== 'center' ) {
4748 newPos
.top
= containerPos
.top
+
4749 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4752 if ( this.horizontalPosition
=== 'before' ) {
4753 newPos
.end
= containerPos
.start
;
4754 } else if ( this.horizontalPosition
=== 'after' ) {
4755 newPos
.start
= containerPos
.end
;
4756 } else if ( this.horizontalPosition
=== 'start' ) {
4757 newPos
.start
= containerPos
.start
;
4758 } else if ( this.horizontalPosition
=== 'end' ) {
4759 newPos
.end
= containerPos
.end
;
4760 } else if ( this.horizontalPosition
=== 'center' ) {
4761 newPos
.left
= containerPos
.left
+
4762 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4765 if ( newPos
.start
!== undefined ) {
4766 if ( direction
=== 'rtl' ) {
4767 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4768 $offsetParent
).outerWidth() - newPos
.start
;
4770 newPos
.left
= newPos
.start
;
4772 delete newPos
.start
;
4774 if ( newPos
.end
!== undefined ) {
4775 if ( direction
=== 'rtl' ) {
4776 newPos
.left
= newPos
.end
;
4778 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4779 $offsetParent
).outerWidth() - newPos
.end
;
4784 // Account for scroll position
4785 if ( newPos
.top
!== '' ) {
4786 newPos
.top
+= scrollTop
;
4788 if ( newPos
.bottom
!== '' ) {
4789 newPos
.bottom
-= scrollTop
;
4791 if ( newPos
.left
!== '' ) {
4792 newPos
.left
+= scrollLeft
;
4794 if ( newPos
.right
!== '' ) {
4795 newPos
.right
-= scrollLeft
;
4798 // Account for scrollbar gutter
4799 if ( newPos
.bottom
!== '' ) {
4800 newPos
.bottom
-= horizScrollbarHeight
;
4802 if ( direction
=== 'rtl' ) {
4803 if ( newPos
.left
!== '' ) {
4804 newPos
.left
-= vertScrollbarWidth
;
4807 if ( newPos
.right
!== '' ) {
4808 newPos
.right
-= vertScrollbarWidth
;
4816 * Element that can be automatically clipped to visible boundaries.
4818 * Whenever the element's natural height changes, you have to call
4819 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4820 * clipping correctly.
4822 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4823 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4824 * then #$clippable will be given a fixed reduced height and/or width and will be made
4825 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4826 * but you can build a static footer by setting #$clippableContainer to an element that contains
4827 * #$clippable and the footer.
4833 * @param {Object} [config] Configuration options
4834 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4835 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4836 * omit to use #$clippable
4838 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4839 // Configuration initialization
4840 config
= config
|| {};
4843 this.$clippable
= null;
4844 this.$clippableContainer
= null;
4845 this.clipping
= false;
4846 this.clippedHorizontally
= false;
4847 this.clippedVertically
= false;
4848 this.$clippableScrollableContainer
= null;
4849 this.$clippableScroller
= null;
4850 this.$clippableWindow
= null;
4851 this.idealWidth
= null;
4852 this.idealHeight
= null;
4853 this.onClippableScrollHandler
= this.clip
.bind( this );
4854 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4857 if ( config
.$clippableContainer
) {
4858 this.setClippableContainer( config
.$clippableContainer
);
4860 this.setClippableElement( config
.$clippable
|| this.$element
);
4866 * Set clippable element.
4868 * If an element is already set, it will be cleaned up before setting up the new element.
4870 * @param {jQuery} $clippable Element to make clippable
4872 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4873 if ( this.$clippable
) {
4874 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4875 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4876 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4879 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4884 * Set clippable container.
4886 * This is the container that will be measured when deciding whether to clip. When clipping,
4887 * #$clippable will be resized in order to keep the clippable container fully visible.
4889 * If the clippable container is unset, #$clippable will be used.
4891 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4893 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4894 this.$clippableContainer
= $clippableContainer
;
4895 if ( this.$clippable
) {
4903 * Do not turn clipping on until after the element is attached to the DOM and visible.
4905 * @param {boolean} [clipping] Enable clipping, omit to toggle
4907 * @return {OO.ui.Element} The element, for chaining
4909 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4910 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4912 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4913 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4914 this.warnedUnattached
= true;
4917 if ( this.clipping
!== clipping
) {
4918 this.clipping
= clipping
;
4920 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4921 // If the clippable container is the root, we have to listen to scroll events and check
4922 // jQuery.scrollTop on the window because of browser inconsistencies
4923 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4924 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4925 this.$clippableScrollableContainer
;
4926 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4927 this.$clippableWindow
= $( this.getElementWindow() )
4928 .on( 'resize', this.onClippableWindowResizeHandler
);
4929 // Initial clip after visible
4932 this.$clippable
.css( {
4940 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4942 this.$clippableScrollableContainer
= null;
4943 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4944 this.$clippableScroller
= null;
4945 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4946 this.$clippableWindow
= null;
4954 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4956 * @return {boolean} Element will be clipped to the visible area
4958 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4959 return this.clipping
;
4963 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4965 * @return {boolean} Part of the element is being clipped
4967 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4968 return this.clippedHorizontally
|| this.clippedVertically
;
4972 * Check if the right of the element is being clipped by the nearest scrollable container.
4974 * @return {boolean} Part of the element is being clipped
4976 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4977 return this.clippedHorizontally
;
4981 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4983 * @return {boolean} Part of the element is being clipped
4985 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4986 return this.clippedVertically
;
4990 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4992 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4993 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4995 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4996 this.idealWidth
= width
;
4997 this.idealHeight
= height
;
4999 if ( !this.clipping
) {
5000 // Update dimensions
5001 this.$clippable
.css( { width
: width
, height
: height
} );
5003 // While clipping, idealWidth and idealHeight are not considered
5007 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5008 * ClippableElement will clip the opposite side when reducing element's width.
5010 * Classes that mix in ClippableElement should override this to return 'right' if their
5011 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
5012 * If your class also mixes in FloatableElement, this is handled automatically.
5014 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5015 * always in pixels, even if they were unset or set to 'auto'.)
5017 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
5019 * @return {string} 'left' or 'right'
5021 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
5022 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
5029 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5030 * ClippableElement will clip the opposite side when reducing element's width.
5032 * Classes that mix in ClippableElement should override this to return 'bottom' if their
5033 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
5034 * If your class also mixes in FloatableElement, this is handled automatically.
5036 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5037 * always in pixels, even if they were unset or set to 'auto'.)
5039 * When in doubt, 'top' is a sane fallback.
5041 * @return {string} 'top' or 'bottom'
5043 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
5044 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
5051 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5052 * when the element's natural height changes.
5054 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5055 * overlapped by, the visible area of the nearest scrollable container.
5057 * Because calling clip() when the natural height changes isn't always possible, we also set
5058 * max-height when the element isn't being clipped. This means that if the element tries to grow
5059 * beyond the edge, something reasonable will happen before clip() is called.
5062 * @return {OO.ui.Element} The element, for chaining
5064 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
5065 var extraHeight
, extraWidth
, viewportSpacing
,
5066 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
5067 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
5068 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
5069 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
5070 // Extra tolerance so that the sloppy code below doesn't result in results that are off
5071 // by one or two pixels. (And also so that we have space to display drop shadows.)
5072 // Chosen by fair dice roll.
5075 if ( !this.clipping
) {
5076 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below
5081 function rectIntersection( a
, b
) {
5083 out
.top
= Math
.max( a
.top
, b
.top
);
5084 out
.left
= Math
.max( a
.left
, b
.left
);
5085 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
5086 out
.right
= Math
.min( a
.right
, b
.right
);
5090 viewportSpacing
= OO
.ui
.getViewportSpacing();
5092 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
5093 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
5094 // Dimensions of the browser window, rather than the element!
5098 right
: document
.documentElement
.clientWidth
,
5099 bottom
: document
.documentElement
.clientHeight
5101 viewportRect
.top
+= viewportSpacing
.top
;
5102 viewportRect
.left
+= viewportSpacing
.left
;
5103 viewportRect
.right
-= viewportSpacing
.right
;
5104 viewportRect
.bottom
-= viewportSpacing
.bottom
;
5106 $viewport
= this.$clippableScrollableContainer
;
5107 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
5108 // Convert into a plain object
5109 viewportRect
= $.extend( {}, viewportRect
);
5112 // Account for scrollbar gutter
5113 direction
= $viewport
.css( 'direction' );
5114 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
5115 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
5116 viewportRect
.bottom
-= horizScrollbarHeight
;
5117 if ( direction
=== 'rtl' ) {
5118 viewportRect
.left
+= vertScrollbarWidth
;
5120 viewportRect
.right
-= vertScrollbarWidth
;
5123 // Add arbitrary tolerance
5124 viewportRect
.top
+= buffer
;
5125 viewportRect
.left
+= buffer
;
5126 viewportRect
.right
-= buffer
;
5127 viewportRect
.bottom
-= buffer
;
5129 $item
= this.$clippableContainer
|| this.$clippable
;
5131 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
5132 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
5134 itemRect
= $item
[ 0 ].getBoundingClientRect();
5135 // Convert into a plain object
5136 itemRect
= $.extend( {}, itemRect
);
5138 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5139 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5140 if ( this.getHorizontalAnchorEdge() === 'right' ) {
5141 itemRect
.left
= viewportRect
.left
;
5143 itemRect
.right
= viewportRect
.right
;
5145 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5146 itemRect
.top
= viewportRect
.top
;
5148 itemRect
.bottom
= viewportRect
.bottom
;
5151 availableRect
= rectIntersection( viewportRect
, itemRect
);
5153 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5154 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5155 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5156 desiredWidth
= Math
.min( desiredWidth
,
5157 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5158 desiredHeight
= Math
.min( desiredHeight
,
5159 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5160 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5161 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5162 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5163 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5164 clipWidth
= allotedWidth
< naturalWidth
;
5165 clipHeight
= allotedHeight
< naturalHeight
;
5168 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5170 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5172 this.$clippable
.css( 'overflowX', 'scroll' );
5173 // eslint-disable-next-line no-void
5174 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5175 this.$clippable
.css( {
5176 width
: Math
.max( 0, allotedWidth
),
5180 this.$clippable
.css( {
5182 width
: this.idealWidth
|| '',
5183 maxWidth
: Math
.max( 0, allotedWidth
)
5187 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5189 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5191 this.$clippable
.css( 'overflowY', 'scroll' );
5192 // eslint-disable-next-line no-void
5193 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5194 this.$clippable
.css( {
5195 height
: Math
.max( 0, allotedHeight
),
5199 this.$clippable
.css( {
5201 height
: this.idealHeight
|| '',
5202 maxHeight
: Math
.max( 0, allotedHeight
)
5206 // If we stopped clipping in at least one of the dimensions
5207 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5208 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5211 this.clippedHorizontally
= clipWidth
;
5212 this.clippedVertically
= clipHeight
;
5218 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5219 * By default, each popup has an anchor that points toward its origin.
5220 * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
5222 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5226 * var popup = new OO.ui.PopupWidget( {
5227 * $content: $( '<p>Hi there!</p>' ),
5232 * $( document.body ).append( popup.$element );
5233 * // To display the popup, toggle the visibility to 'true'.
5234 * popup.toggle( true );
5236 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5239 * @extends OO.ui.Widget
5240 * @mixins OO.ui.mixin.LabelElement
5241 * @mixins OO.ui.mixin.ClippableElement
5242 * @mixins OO.ui.mixin.FloatableElement
5245 * @param {Object} [config] Configuration options
5246 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5247 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5248 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5249 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5250 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5251 * of $floatableContainer
5252 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5253 * of $floatableContainer
5254 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5255 * endwards (right/left) to the vertical center of $floatableContainer
5256 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5257 * startwards (left/right) to the vertical center of $floatableContainer
5258 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5259 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in
5260 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5261 * move the popup as far downwards as possible.
5262 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in
5263 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5264 * move the popup as far upwards as possible.
5265 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the
5266 * center of the popup with the center of $floatableContainer.
5267 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5268 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5269 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5270 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5271 * desired direction to display the popup without clipping
5272 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5273 * See the [OOUI docs on MediaWiki][3] for an example.
5274 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5275 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a
5277 * @cfg {jQuery} [$content] Content to append to the popup's body
5278 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5279 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5280 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5281 * This config option is only relevant if #autoClose is set to `true`. See the
5282 * [OOUI documentation on MediaWiki][2] for an example.
5283 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5284 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5286 * @cfg {boolean} [padded=false] Add padding to the popup's body
5288 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5289 // Configuration initialization
5290 config
= config
|| {};
5292 // Parent constructor
5293 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5295 // Properties (must be set before ClippableElement constructor call)
5296 this.$body
= $( '<div>' );
5297 this.$popup
= $( '<div>' );
5299 // Mixin constructors
5300 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5301 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {
5302 $clippable
: this.$body
,
5303 $clippableContainer
: this.$popup
5305 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5308 this.$anchor
= $( '<div>' );
5309 // If undefined, will be computed lazily in computePosition()
5310 this.$container
= config
.$container
;
5311 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5312 this.autoClose
= !!config
.autoClose
;
5313 this.transitionTimeout
= null;
5314 this.anchored
= false;
5315 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5316 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5319 this.setSize( config
.width
, config
.height
);
5320 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5321 this.setAlignment( config
.align
|| 'center' );
5322 this.setPosition( config
.position
|| 'below' );
5323 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5324 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5325 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5326 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5328 .addClass( 'oo-ui-popupWidget-popup' )
5329 .append( this.$body
);
5331 .addClass( 'oo-ui-popupWidget' )
5332 .append( this.$popup
, this.$anchor
);
5333 // Move content, which was added to #$element by OO.ui.Widget, to the body
5334 // FIXME This is gross, we should use '$body' or something for the config
5335 if ( config
.$content
instanceof $ ) {
5336 this.$body
.append( config
.$content
);
5339 if ( config
.padded
) {
5340 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5343 if ( config
.head
) {
5344 this.closeButton
= new OO
.ui
.ButtonWidget( {
5348 this.closeButton
.connect( this, {
5349 click
: 'onCloseButtonClick'
5351 this.$head
= $( '<div>' )
5352 .addClass( 'oo-ui-popupWidget-head' )
5353 .append( this.$label
, this.closeButton
.$element
);
5354 this.$popup
.prepend( this.$head
);
5357 if ( config
.$footer
) {
5358 this.$footer
= $( '<div>' )
5359 .addClass( 'oo-ui-popupWidget-footer' )
5360 .append( config
.$footer
);
5361 this.$popup
.append( this.$footer
);
5364 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5365 // that reference properties not initialized at that time of parent class construction
5366 // TODO: Find a better way to handle post-constructor setup
5367 this.visible
= false;
5368 this.$element
.addClass( 'oo-ui-element-hidden' );
5373 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5374 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5375 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5376 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5383 * The popup is ready: it is visible and has been positioned and clipped.
5389 * Handles document mouse down events.
5392 * @param {MouseEvent} e Mouse down event
5394 OO
.ui
.PopupWidget
.prototype.onDocumentMouseDown = function ( e
) {
5397 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5399 this.toggle( false );
5404 * Bind document mouse down listener.
5408 OO
.ui
.PopupWidget
.prototype.bindDocumentMouseDownListener = function () {
5409 // Capture clicks outside popup
5410 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5411 // We add 'click' event because iOS safari needs to respond to this event.
5412 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5413 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5414 // of occasionally not emitting 'click' properly, that event seems to be the standard
5415 // that it should be emitting, so we add it to this and will operate the event handler
5416 // on whichever of these events was triggered first
5417 this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5421 * Handles close button click events.
5425 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5426 if ( this.isVisible() ) {
5427 this.toggle( false );
5432 * Unbind document mouse down listener.
5436 OO
.ui
.PopupWidget
.prototype.unbindDocumentMouseDownListener = function () {
5437 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5438 this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5442 * Handles document key down events.
5445 * @param {KeyboardEvent} e Key down event
5447 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5449 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5452 this.toggle( false );
5454 e
.stopPropagation();
5459 * Bind document key down listener.
5463 OO
.ui
.PopupWidget
.prototype.bindDocumentKeyDownListener = function () {
5464 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5468 * Unbind document key down listener.
5472 OO
.ui
.PopupWidget
.prototype.unbindDocumentKeyDownListener = function () {
5473 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5477 * Show, hide, or toggle the visibility of the anchor.
5479 * @param {boolean} [show] Show anchor, omit to toggle
5481 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5482 show
= show
=== undefined ? !this.anchored
: !!show
;
5484 if ( this.anchored
!== show
) {
5486 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5487 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5489 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5490 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5492 this.anchored
= show
;
5497 * Change which edge the anchor appears on.
5499 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5501 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5502 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5503 throw new Error( 'Invalid value for edge: ' + edge
);
5505 if ( this.anchorEdge
!== null ) {
5506 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5508 this.anchorEdge
= edge
;
5509 if ( this.anchored
) {
5510 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5515 * Check if the anchor is visible.
5517 * @return {boolean} Anchor is visible
5519 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5520 return this.anchored
;
5524 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5525 * `.toggle( true )` after its #$element is attached to the DOM.
5527 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5528 * it in the right place and with the right dimensions only work correctly while it is attached.
5529 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5530 * strictly enforced, so currently it only generates a warning in the browser console.
5535 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5536 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5537 show
= show
=== undefined ? !this.isVisible() : !!show
;
5539 change
= show
!== this.isVisible();
5541 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5542 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5543 this.warnedUnattached
= true;
5545 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5546 // Fall back to the parent node if the floatableContainer is not set
5547 this.setFloatableContainer( this.$element
.parent() );
5550 if ( change
&& show
&& this.autoFlip
) {
5551 // Reset auto-flipping before showing the popup again. It's possible we no longer need to
5552 // flip (e.g. if the user scrolled).
5553 this.isAutoFlipped
= false;
5557 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5560 this.togglePositioning( show
&& !!this.$floatableContainer
);
5563 if ( this.autoClose
) {
5564 this.bindDocumentMouseDownListener();
5565 this.bindDocumentKeyDownListener();
5567 this.updateDimensions();
5568 this.toggleClipping( true );
5570 if ( this.autoFlip
) {
5571 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5572 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5573 // If opening the popup in the normal direction causes it to be clipped,
5574 // open in the opposite one instead
5575 normalHeight
= this.$element
.height();
5576 this.isAutoFlipped
= !this.isAutoFlipped
;
5578 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5579 // If that also causes it to be clipped, open in whichever direction
5580 // we have more space
5581 oppositeHeight
= this.$element
.height();
5582 if ( oppositeHeight
< normalHeight
) {
5583 this.isAutoFlipped
= !this.isAutoFlipped
;
5589 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5590 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5591 // If opening the popup in the normal direction causes it to be clipped,
5592 // open in the opposite one instead
5593 normalWidth
= this.$element
.width();
5594 this.isAutoFlipped
= !this.isAutoFlipped
;
5595 // Due to T180173 horizontally clipped PopupWidgets have messed up
5596 // dimensions, which causes positioning to be off. Toggle clipping back and
5597 // forth to work around.
5598 this.toggleClipping( false );
5600 this.toggleClipping( true );
5601 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5602 // If that also causes it to be clipped, open in whichever direction
5603 // we have more space
5604 oppositeWidth
= this.$element
.width();
5605 if ( oppositeWidth
< normalWidth
) {
5606 this.isAutoFlipped
= !this.isAutoFlipped
;
5607 // Due to T180173, horizontally clipped PopupWidgets have messed up
5608 // dimensions, which causes positioning to be off. Toggle clipping
5609 // back and forth to work around.
5610 this.toggleClipping( false );
5612 this.toggleClipping( true );
5619 this.emit( 'ready' );
5621 this.toggleClipping( false );
5622 if ( this.autoClose
) {
5623 this.unbindDocumentMouseDownListener();
5624 this.unbindDocumentKeyDownListener();
5633 * Set the size of the popup.
5635 * Changing the size may also change the popup's position depending on the alignment.
5637 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5638 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5639 * @param {boolean} [transition=false] Use a smooth transition
5642 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5643 this.width
= width
!== undefined ? width
: 320;
5644 this.height
= height
!== undefined ? height
: null;
5645 if ( this.isVisible() ) {
5646 this.updateDimensions( transition
);
5651 * Update the size and position.
5653 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5654 * be called automatically.
5656 * @param {boolean} [transition=false] Use a smooth transition
5659 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5662 // Prevent transition from being interrupted
5663 clearTimeout( this.transitionTimeout
);
5665 // Enable transition
5666 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5672 // Prevent transitioning after transition is complete
5673 this.transitionTimeout
= setTimeout( function () {
5674 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5677 // Prevent transitioning immediately
5678 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5685 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5686 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
,
5687 anchorPos
, anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
,
5688 floatablePos
, offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5690 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5691 popupPositionOppositeMap
= {
5699 'force-left': 'backwards',
5700 'force-right': 'forwards'
5703 'force-left': 'forwards',
5704 'force-right': 'backwards'
5716 backwards
: this.anchored
? 'before' : 'end'
5724 if ( !this.$container
) {
5725 // Lazy-initialize $container if not specified in constructor
5726 this.$container
= $( this.getClosestScrollableElementContainer() );
5728 direction
= this.$container
.css( 'direction' );
5730 // Set height and width before we do anything else, since it might cause our measurements
5731 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5733 width
: this.width
!== null ? this.width
: 'auto',
5734 height
: this.height
!== null ? this.height
: 'auto'
5737 align
= alignMap
[ direction
][ this.align
] || this.align
;
5738 popupPosition
= this.popupPosition
;
5739 if ( this.isAutoFlipped
) {
5740 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5743 // If the popup is positioned before or after, then the anchor positioning is vertical,
5744 // otherwise horizontal
5745 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5746 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5747 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5748 near
= vertical
? 'top' : 'left';
5749 far
= vertical
? 'bottom' : 'right';
5750 sizeProp
= vertical
? 'Height' : 'Width';
5751 popupSize
= vertical
?
5752 ( this.height
|| this.$popup
.height() ) :
5753 ( this.width
|| this.$popup
.width() );
5755 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5756 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5757 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5760 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5761 // Find out which property FloatableElement used for positioning, and adjust that value
5762 positionProp
= vertical
?
5763 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5764 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5766 // Figure out where the near and far edges of the popup and $floatableContainer are
5767 floatablePos
= this.$floatableContainer
.offset();
5768 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5769 // Measure where the offsetParent is and compute our position based on that and parentPosition
5770 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5771 { top
: 0, left
: 0 } :
5772 this.$element
.offsetParent().offset();
5774 if ( positionProp
=== near
) {
5775 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5776 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5778 popupPos
[ far
] = offsetParentPos
[ near
] +
5779 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5780 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5783 if ( this.anchored
) {
5784 // Position the anchor (which is positioned relative to the popup) to point to
5785 // $floatableContainer
5786 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5787 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5789 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more
5790 // space this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use
5791 // scrollWidth/Height
5792 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5793 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5794 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5795 // Not enough space for the anchor on the start side; pull the popup startwards
5796 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5797 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5798 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5799 // Not enough space for the anchor on the end side; pull the popup endwards
5800 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5801 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5803 positionAdjustment
= 0;
5806 positionAdjustment
= 0;
5809 // Check if the popup will go beyond the edge of this.$container
5810 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5811 { top
: 0, left
: 0 } :
5812 this.$container
.offset();
5813 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5814 if ( this.$container
[ 0 ] === document
.documentElement
) {
5815 viewportSpacing
= OO
.ui
.getViewportSpacing();
5816 containerPos
[ near
] += viewportSpacing
[ near
];
5817 containerPos
[ far
] -= viewportSpacing
[ far
];
5819 // Take into account how much the popup will move because of the adjustments we're going to make
5820 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5821 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5822 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5823 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5824 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5825 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5826 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5827 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5828 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5829 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5832 if ( this.anchored
) {
5833 // Adjust anchorOffset for positionAdjustment
5834 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5836 // Position the anchor
5837 anchorCss
[ start
] = anchorOffset
;
5838 this.$anchor
.css( anchorCss
);
5841 // Move the popup if needed
5842 parentPosition
[ positionProp
] += positionAdjustment
;
5844 return parentPosition
;
5848 * Set popup alignment
5850 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5851 * `backwards` or `forwards`.
5853 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5854 // Validate alignment
5855 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5858 this.align
= 'center';
5864 * Get popup alignment
5866 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5867 * `backwards` or `forwards`.
5869 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5874 * Change the positioning of the popup.
5876 * @param {string} position 'above', 'below', 'before' or 'after'
5878 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5879 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5882 this.popupPosition
= position
;
5887 * Get popup positioning.
5889 * @return {string} 'above', 'below', 'before' or 'after'
5891 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5892 return this.popupPosition
;
5896 * Set popup auto-flipping.
5898 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5899 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5900 * desired direction to display the popup without clipping
5902 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5903 autoFlip
= !!autoFlip
;
5905 if ( this.autoFlip
!== autoFlip
) {
5906 this.autoFlip
= autoFlip
;
5911 * Set which elements will not close the popup when clicked.
5913 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
5915 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
5917 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
5918 this.$autoCloseIgnore
= $autoCloseIgnore
;
5922 * Get an ID of the body element, this can be used as the
5923 * `aria-describedby` attribute for an input field.
5925 * @return {string} The ID of the body element
5927 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5928 var id
= this.$body
.attr( 'id' );
5929 if ( id
=== undefined ) {
5930 id
= OO
.ui
.generateElementId();
5931 this.$body
.attr( 'id', id
);
5937 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5938 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5939 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5940 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5946 * @param {Object} [config] Configuration options
5947 * @cfg {Object} [popup] Configuration to pass to popup
5948 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5950 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5951 // Configuration initialization
5952 config
= config
|| {};
5955 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5958 $floatableContainer
: this.$element
5962 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5972 * @return {OO.ui.PopupWidget} Popup widget
5974 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5979 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5980 * which is used to display additional information or options.
5983 * // A PopupButtonWidget.
5984 * var popupButton = new OO.ui.PopupButtonWidget( {
5985 * label: 'Popup button with options',
5988 * $content: $( '<p>Additional options here.</p>' ),
5990 * align: 'force-left'
5993 * // Append the button to the DOM.
5994 * $( document.body ).append( popupButton.$element );
5997 * @extends OO.ui.ButtonWidget
5998 * @mixins OO.ui.mixin.PopupElement
6001 * @param {Object} [config] Configuration options
6002 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful
6003 * in cases where the expanded popup is larger than its containing `<div>`. The specified overlay
6004 * layer is usually on top of the containing `<div>` and has a larger area. By default, the popup
6005 * uses relative positioning.
6006 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
6008 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
6009 // Configuration initialization
6010 config
= config
|| {};
6012 // Parent constructor
6013 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
6015 // Mixin constructors
6016 OO
.ui
.mixin
.PopupElement
.call( this, config
);
6019 this.$overlay
= ( config
.$overlay
=== true ?
6020 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
6023 this.connect( this, {
6028 this.$element
.addClass( 'oo-ui-popupButtonWidget' );
6030 .addClass( 'oo-ui-popupButtonWidget-popup' )
6031 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6032 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6033 this.$overlay
.append( this.popup
.$element
);
6038 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
6039 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
6044 * Handle the button action being triggered.
6048 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
6049 this.popup
.toggle();
6053 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6055 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6060 * @mixins OO.ui.mixin.GroupElement
6063 * @param {Object} [config] Configuration options
6065 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
6066 // Mixin constructors
6067 OO
.ui
.mixin
.GroupElement
.call( this, config
);
6072 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
6077 * Set the disabled state of the widget.
6079 * This will also update the disabled state of child widgets.
6081 * @param {boolean} disabled Disable widget
6083 * @return {OO.ui.Widget} The widget, for chaining
6085 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
6089 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6090 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
6092 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6094 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6095 this.items
[ i
].updateDisabled();
6103 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6105 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group.
6106 * This allows bidirectional communication.
6108 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6116 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
6123 * Check if widget is disabled.
6125 * Checks parent if present, making disabled state inheritable.
6127 * @return {boolean} Widget is disabled
6129 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
6130 return this.disabled
||
6131 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
6135 * Set group element is in.
6137 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6139 * @return {OO.ui.Widget} The widget, for chaining
6141 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
6143 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6144 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
6146 // Initialize item disabled states
6147 this.updateDisabled();
6153 * OptionWidgets are special elements that can be selected and configured with data. The
6154 * data is often unique for each option, but it does not have to be. OptionWidgets are used
6155 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6156 * and examples, please see the [OOUI documentation on MediaWiki][1].
6158 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6161 * @extends OO.ui.Widget
6162 * @mixins OO.ui.mixin.ItemWidget
6163 * @mixins OO.ui.mixin.LabelElement
6164 * @mixins OO.ui.mixin.FlaggedElement
6165 * @mixins OO.ui.mixin.AccessKeyedElement
6166 * @mixins OO.ui.mixin.TitledElement
6169 * @param {Object} [config] Configuration options
6171 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
6172 // Configuration initialization
6173 config
= config
|| {};
6175 // Parent constructor
6176 OO
.ui
.OptionWidget
.parent
.call( this, config
);
6178 // Mixin constructors
6179 OO
.ui
.mixin
.ItemWidget
.call( this );
6180 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6181 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6182 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6183 OO
.ui
.mixin
.TitledElement
.call( this, config
);
6186 this.highlighted
= false;
6187 this.pressed
= false;
6188 this.setSelected( !!config
.selected
);
6192 .data( 'oo-ui-optionWidget', this )
6193 // Allow programmatic focussing (and by access key), but not tabbing
6194 .attr( 'tabindex', '-1' )
6195 .attr( 'role', 'option' )
6196 .addClass( 'oo-ui-optionWidget' )
6197 .append( this.$label
);
6202 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6203 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6204 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6205 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6206 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6207 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.TitledElement
);
6209 /* Static Properties */
6212 * Whether this option can be selected. See #setSelected.
6216 * @property {boolean}
6218 OO
.ui
.OptionWidget
.static.selectable
= true;
6221 * Whether this option can be highlighted. See #setHighlighted.
6225 * @property {boolean}
6227 OO
.ui
.OptionWidget
.static.highlightable
= true;
6230 * Whether this option can be pressed. See #setPressed.
6234 * @property {boolean}
6236 OO
.ui
.OptionWidget
.static.pressable
= true;
6239 * Whether this option will be scrolled into view when it is selected.
6243 * @property {boolean}
6245 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6250 * Check if the option can be selected.
6252 * @return {boolean} Item is selectable
6254 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6255 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6259 * Check if the option can be highlighted. A highlight indicates that the option
6260 * may be selected when a user presses Enter key or clicks. Disabled items cannot
6263 * @return {boolean} Item is highlightable
6265 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6266 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6270 * Check if the option can be pressed. The pressed state occurs when a user mouses
6271 * down on an item, but has not yet let go of the mouse.
6273 * @return {boolean} Item is pressable
6275 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6276 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6280 * Check if the option is selected.
6282 * @return {boolean} Item is selected
6284 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6285 return this.selected
;
6289 * Check if the option is highlighted. A highlight indicates that the
6290 * item may be selected when a user presses Enter key or clicks.
6292 * @return {boolean} Item is highlighted
6294 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6295 return this.highlighted
;
6299 * Check if the option is pressed. The pressed state occurs when a user mouses
6300 * down on an item, but has not yet let go of the mouse. The item may appear
6301 * selected, but it will not be selected until the user releases the mouse.
6303 * @return {boolean} Item is pressed
6305 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6306 return this.pressed
;
6310 * Set the option’s selected state. In general, all modifications to the selection
6311 * should be handled by the SelectWidget’s
6312 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
6314 * @param {boolean} [state=false] Select option
6316 * @return {OO.ui.Widget} The widget, for chaining
6318 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6319 if ( this.constructor.static.selectable
) {
6320 this.selected
= !!state
;
6322 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6323 .attr( 'aria-selected', state
.toString() );
6324 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6325 this.scrollElementIntoView();
6327 this.updateThemeClasses();
6333 * Set the option’s highlighted state. In general, all programmatic
6334 * modifications to the highlight should be handled by the
6335 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6336 * method instead of this method.
6338 * @param {boolean} [state=false] Highlight option
6340 * @return {OO.ui.Widget} The widget, for chaining
6342 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6343 if ( this.constructor.static.highlightable
) {
6344 this.highlighted
= !!state
;
6345 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6346 this.updateThemeClasses();
6352 * Set the option’s pressed state. In general, all
6353 * programmatic modifications to the pressed state should be handled by the
6354 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6355 * method instead of this method.
6357 * @param {boolean} [state=false] Press option
6359 * @return {OO.ui.Widget} The widget, for chaining
6361 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6362 if ( this.constructor.static.pressable
) {
6363 this.pressed
= !!state
;
6364 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6365 this.updateThemeClasses();
6371 * Get text to match search strings against.
6373 * The default implementation returns the label text, but subclasses
6374 * can override this to provide more complex behavior.
6376 * @return {string|boolean} String to match search string against
6378 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6379 var label
= this.getLabel();
6380 return typeof label
=== 'string' ? label
: this.$label
.text();
6384 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6385 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6386 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6389 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For
6390 * more information, please see the [OOUI documentation on MediaWiki][1].
6393 * // A select widget with three options.
6394 * var select = new OO.ui.SelectWidget( {
6396 * new OO.ui.OptionWidget( {
6398 * label: 'Option One',
6400 * new OO.ui.OptionWidget( {
6402 * label: 'Option Two',
6404 * new OO.ui.OptionWidget( {
6406 * label: 'Option Three',
6410 * $( document.body ).append( select.$element );
6412 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6416 * @extends OO.ui.Widget
6417 * @mixins OO.ui.mixin.GroupWidget
6420 * @param {Object} [config] Configuration options
6421 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6422 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6423 * the [OOUI documentation on MediaWiki] [2] for examples.
6424 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6425 * @cfg {boolean} [multiselect] Allow for multiple selections
6427 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6428 // Configuration initialization
6429 config
= config
|| {};
6431 // Parent constructor
6432 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6434 // Mixin constructors
6435 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {
6436 $group
: this.$element
6440 this.pressed
= false;
6441 this.selecting
= null;
6442 this.multiselect
= !!config
.multiselect
;
6443 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
6444 this.onDocumentMouseMoveHandler
= this.onDocumentMouseMove
.bind( this );
6445 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
6446 this.onDocumentKeyPressHandler
= this.onDocumentKeyPress
.bind( this );
6447 this.keyPressBuffer
= '';
6448 this.keyPressBufferTimer
= null;
6449 this.blockMouseOverEvents
= 0;
6452 this.connect( this, {
6456 focusin
: this.onFocus
.bind( this ),
6457 mousedown
: this.onMouseDown
.bind( this ),
6458 mouseover
: this.onMouseOver
.bind( this ),
6459 mouseleave
: this.onMouseLeave
.bind( this )
6464 // -depressed is a deprecated alias of -unpressed
6465 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-unpressed oo-ui-selectWidget-depressed' )
6466 .attr( 'role', 'listbox' );
6467 this.setFocusOwner( this.$element
);
6468 if ( Array
.isArray( config
.items
) ) {
6469 this.addItems( config
.items
);
6475 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6476 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6483 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6485 * @param {OO.ui.OptionWidget|null} item Highlighted item
6491 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6492 * pressed state of an option.
6494 * @param {OO.ui.OptionWidget|null} item Pressed item
6500 * A `select` event is emitted when the selection is modified programmatically with the #selectItem
6503 * @param {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} items Currently selected items
6509 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6511 * @param {OO.ui.OptionWidget} item Chosen item
6512 * @param {boolean} selected Item is selected
6518 * An `add` event is emitted when options are added to the select with the #addItems method.
6520 * @param {OO.ui.OptionWidget[]} items Added items
6521 * @param {number} index Index of insertion point
6527 * A `remove` event is emitted when options are removed from the select with the #clearItems
6528 * or #removeItems methods.
6530 * @param {OO.ui.OptionWidget[]} items Removed items
6533 /* Static methods */
6536 * Normalize text for filter matching
6538 * @param {string} text Text
6539 * @return {string} Normalized text
6541 OO
.ui
.SelectWidget
.static.normalizeForMatching = function ( text
) {
6542 // Replace trailing whitespace, normalize multiple spaces and make case insensitive
6543 var normalized
= text
.trim().replace( /\s+/, ' ' ).toLowerCase();
6545 // Normalize Unicode
6546 // eslint-disable-next-line no-restricted-properties
6547 if ( normalized
.normalize
) {
6548 // eslint-disable-next-line no-restricted-properties
6549 normalized
= normalized
.normalize();
6557 * Handle focus events
6560 * @param {jQuery.Event} event
6562 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6564 if ( event
.target
=== this.$element
[ 0 ] ) {
6565 // This widget was focussed, e.g. by the user tabbing to it.
6566 // The styles for focus state depend on one of the items being selected.
6567 if ( !this.findSelectedItem() ) {
6568 item
= this.findFirstSelectableItem();
6571 if ( event
.target
.tabIndex
=== -1 ) {
6572 // One of the options got focussed (and the event bubbled up here).
6573 // They can't be tabbed to, but they can be activated using access keys.
6574 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6575 item
= this.findTargetItem( event
);
6577 // There is something actually user-focusable in one of the labels of the options, and
6578 // the user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change
6585 if ( item
.constructor.static.highlightable
) {
6586 this.highlightItem( item
);
6588 this.selectItem( item
);
6592 if ( event
.target
!== this.$element
[ 0 ] ) {
6593 this.$focusOwner
.trigger( 'focus' );
6598 * Handle mouse down events.
6601 * @param {jQuery.Event} e Mouse down event
6602 * @return {undefined/boolean} False to prevent default if event is handled
6604 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6607 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6608 this.togglePressed( true );
6609 item
= this.findTargetItem( e
);
6610 if ( item
&& item
.isSelectable() ) {
6611 this.pressItem( item
);
6612 this.selecting
= item
;
6613 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6614 this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6621 * Handle document mouse up events.
6624 * @param {MouseEvent} e Mouse up event
6625 * @return {undefined/boolean} False to prevent default if event is handled
6627 OO
.ui
.SelectWidget
.prototype.onDocumentMouseUp = function ( e
) {
6630 this.togglePressed( false );
6631 if ( !this.selecting
) {
6632 item
= this.findTargetItem( e
);
6633 if ( item
&& item
.isSelectable() ) {
6634 this.selecting
= item
;
6637 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6638 this.pressItem( null );
6639 this.chooseItem( this.selecting
);
6640 this.selecting
= null;
6643 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6644 this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6650 * Handle document mouse move events.
6653 * @param {MouseEvent} e Mouse move event
6655 OO
.ui
.SelectWidget
.prototype.onDocumentMouseMove = function ( e
) {
6658 if ( !this.isDisabled() && this.pressed
) {
6659 item
= this.findTargetItem( e
);
6660 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6661 this.pressItem( item
);
6662 this.selecting
= item
;
6668 * Handle mouse over events.
6671 * @param {jQuery.Event} e Mouse over event
6672 * @return {undefined/boolean} False to prevent default if event is handled
6674 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6676 if ( this.blockMouseOverEvents
) {
6679 if ( !this.isDisabled() ) {
6680 item
= this.findTargetItem( e
);
6681 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6687 * Handle mouse leave events.
6690 * @param {jQuery.Event} e Mouse over event
6691 * @return {undefined/boolean} False to prevent default if event is handled
6693 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6694 if ( !this.isDisabled() ) {
6695 this.highlightItem( null );
6701 * Handle document key down events.
6704 * @param {KeyboardEvent} e Key down event
6706 OO
.ui
.SelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
6709 currentItem
= this.findHighlightedItem(),
6710 firstItem
= this.getItems()[ 0 ];
6712 if ( !this.isDisabled() && this.isVisible() ) {
6713 switch ( e
.keyCode
) {
6714 case OO
.ui
.Keys
.ENTER
:
6715 if ( currentItem
) {
6716 // Was only highlighted, now let's select it. No-op if already selected.
6717 this.chooseItem( currentItem
);
6722 case OO
.ui
.Keys
.LEFT
:
6723 this.clearKeyPressBuffer();
6724 nextItem
= currentItem
? this.findRelativeSelectableItem( currentItem
, -1 ) : firstItem
;
6727 case OO
.ui
.Keys
.DOWN
:
6728 case OO
.ui
.Keys
.RIGHT
:
6729 this.clearKeyPressBuffer();
6730 nextItem
= currentItem
? this.findRelativeSelectableItem( currentItem
, 1 ) : firstItem
;
6733 case OO
.ui
.Keys
.ESCAPE
:
6734 case OO
.ui
.Keys
.TAB
:
6735 if ( currentItem
) {
6736 currentItem
.setHighlighted( false );
6738 this.unbindDocumentKeyDownListener();
6739 this.unbindDocumentKeyPressListener();
6740 // Don't prevent tabbing away / defocusing
6746 if ( nextItem
.constructor.static.highlightable
) {
6747 this.highlightItem( nextItem
);
6749 this.chooseItem( nextItem
);
6751 this.scrollItemIntoView( nextItem
);
6756 e
.stopPropagation();
6762 * Bind document key down listener.
6766 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyDownListener = function () {
6767 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6771 * Unbind document key down listener.
6775 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
6776 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6780 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6782 * @param {OO.ui.OptionWidget} item Item to scroll into view
6784 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6786 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic
6787 // scrolling and around 100-150 ms after it is finished.
6788 this.blockMouseOverEvents
++;
6789 item
.scrollElementIntoView().done( function () {
6790 setTimeout( function () {
6791 widget
.blockMouseOverEvents
--;
6797 * Clear the key-press buffer
6801 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6802 if ( this.keyPressBufferTimer
) {
6803 clearTimeout( this.keyPressBufferTimer
);
6804 this.keyPressBufferTimer
= null;
6806 this.keyPressBuffer
= '';
6810 * Handle key press events.
6813 * @param {KeyboardEvent} e Key press event
6814 * @return {undefined/boolean} False to prevent default if event is handled
6816 OO
.ui
.SelectWidget
.prototype.onDocumentKeyPress = function ( e
) {
6817 var c
, filter
, item
;
6819 if ( !e
.charCode
) {
6820 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6821 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6826 // eslint-disable-next-line no-restricted-properties
6827 if ( String
.fromCodePoint
) {
6828 // eslint-disable-next-line no-restricted-properties
6829 c
= String
.fromCodePoint( e
.charCode
);
6831 c
= String
.fromCharCode( e
.charCode
);
6834 if ( this.keyPressBufferTimer
) {
6835 clearTimeout( this.keyPressBufferTimer
);
6837 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6839 item
= this.findHighlightedItem() || this.findSelectedItem();
6841 if ( this.keyPressBuffer
=== c
) {
6842 // Common (if weird) special case: typing "xxxx" will cycle through all
6843 // the items beginning with "x".
6845 item
= this.findRelativeSelectableItem( item
, 1 );
6848 this.keyPressBuffer
+= c
;
6851 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6852 if ( !item
|| !filter( item
) ) {
6853 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6856 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6857 this.highlightItem( item
);
6859 this.chooseItem( item
);
6861 this.scrollItemIntoView( item
);
6865 e
.stopPropagation();
6869 * Get a matcher for the specific string
6872 * @param {string} query String to match against items
6873 * @param {string} [mode='prefix'] Matching mode: 'substring', 'prefix', or 'exact'
6874 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6876 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( query
, mode
) {
6877 var normalizeForMatching
= this.constructor.static.normalizeForMatching
,
6878 normalizedQuery
= normalizeForMatching( query
);
6880 // Support deprecated exact=true argument
6881 if ( mode
=== true ) {
6885 return function ( item
) {
6886 var matchText
= normalizeForMatching( item
.getMatchText() );
6888 if ( normalizedQuery
=== '' ) {
6889 // Empty string matches all, except if we are in 'exact'
6890 // mode, where it doesn't match at all
6891 return mode
!== 'exact';
6896 return matchText
=== normalizedQuery
;
6898 return matchText
.indexOf( normalizedQuery
) !== -1;
6901 return matchText
.indexOf( normalizedQuery
) === 0;
6907 * Bind document key press listener.
6911 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyPressListener = function () {
6912 this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6916 * Unbind document key down listener.
6918 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6923 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
6924 this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6925 this.clearKeyPressBuffer();
6929 * Visibility change handler
6932 * @param {boolean} visible
6934 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6936 this.clearKeyPressBuffer();
6941 * Get the closest item to a jQuery.Event.
6944 * @param {jQuery.Event} e
6945 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6947 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6948 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6949 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6952 return $option
.data( 'oo-ui-optionWidget' ) || null;
6956 * Find all selected items, if there are any. If the widget allows for multiselect
6957 * it will return an array of selected options. If the widget doesn't allow for
6958 * multiselect, it will return the selected option or null if no item is selected.
6960 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
6961 * then return an array of selected items (or empty array),
6962 * if the widget is not multiselect, return a single selected item, or `null`
6963 * if no item is selected
6965 OO
.ui
.SelectWidget
.prototype.findSelectedItems = function () {
6966 var selected
= this.items
.filter( function ( item
) {
6967 return item
.isSelected();
6970 return this.multiselect
?
6972 selected
[ 0 ] || null;
6976 * Find selected item.
6978 * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
6979 * then return an array of selected items (or empty array),
6980 * if the widget is not multiselect, return a single selected item, or `null`
6981 * if no item is selected
6983 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
6984 return this.findSelectedItems();
6988 * Find highlighted item.
6990 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6992 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6995 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6996 if ( this.items
[ i
].isHighlighted() ) {
6997 return this.items
[ i
];
7004 * Toggle pressed state.
7006 * Press is a state that occurs when a user mouses down on an item, but
7007 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
7008 * until the user releases the mouse.
7010 * @param {boolean} pressed An option is being pressed
7012 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
7013 if ( pressed
=== undefined ) {
7014 pressed
= !this.pressed
;
7016 if ( pressed
!== this.pressed
) {
7018 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
7019 // -depressed is a deprecated alias of -unpressed
7020 .toggleClass( 'oo-ui-selectWidget-unpressed oo-ui-selectWidget-depressed', !pressed
);
7021 this.pressed
= pressed
;
7026 * Highlight an option. If the `item` param is omitted, no options will be highlighted
7027 * and any existing highlight will be removed. The highlight is mutually exclusive.
7029 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
7032 * @return {OO.ui.Widget} The widget, for chaining
7034 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
7035 var i
, len
, highlighted
,
7038 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7039 highlighted
= this.items
[ i
] === item
;
7040 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
7041 this.items
[ i
].setHighlighted( highlighted
);
7047 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7049 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7051 this.emit( 'highlight', item
);
7058 * Fetch an item by its label.
7060 * @param {string} label Label of the item to select.
7061 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7062 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7064 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
7066 len
= this.items
.length
,
7067 filter
= this.getItemMatcher( label
, 'exact' );
7069 for ( i
= 0; i
< len
; i
++ ) {
7070 item
= this.items
[ i
];
7071 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7078 filter
= this.getItemMatcher( label
, 'prefix' );
7079 for ( i
= 0; i
< len
; i
++ ) {
7080 item
= this.items
[ i
];
7081 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7097 * Programmatically select an option by its label. If the item does not exist,
7098 * all options will be deselected.
7100 * @param {string} [label] Label of the item to select.
7101 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7104 * @return {OO.ui.Widget} The widget, for chaining
7106 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
7107 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
7108 if ( label
=== undefined || !itemFromLabel
) {
7109 return this.selectItem();
7111 return this.selectItem( itemFromLabel
);
7115 * Programmatically select an option by its data. If the `data` parameter is omitted,
7116 * or if the item does not exist, all options will be deselected.
7118 * @param {Object|string} [data] Value of the item to select, omit to deselect all
7121 * @return {OO.ui.Widget} The widget, for chaining
7123 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
7124 var itemFromData
= this.findItemFromData( data
);
7125 if ( data
=== undefined || !itemFromData
) {
7126 return this.selectItem();
7128 return this.selectItem( itemFromData
);
7132 * Programmatically unselect an option by its reference. If the widget
7133 * allows for multiple selections, there may be other items still selected;
7134 * otherwise, no items will be selected.
7135 * If no item is given, all selected items will be unselected.
7137 * @param {OO.ui.OptionWidget} [item] Item to unselect
7140 * @return {OO.ui.Widget} The widget, for chaining
7142 OO
.ui
.SelectWidget
.prototype.unselectItem = function ( item
) {
7144 item
.setSelected( false );
7146 this.items
.forEach( function ( item
) {
7147 item
.setSelected( false );
7151 this.emit( 'select', this.findSelectedItems() );
7156 * Programmatically select an option by its reference. If the `item` parameter is omitted,
7157 * all options will be deselected.
7159 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7162 * @return {OO.ui.Widget} The widget, for chaining
7164 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
7165 var i
, len
, selected
,
7168 if ( this.multiselect
&& item
) {
7169 // Select the item directly
7170 item
.setSelected( true );
7172 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7173 selected
= this.items
[ i
] === item
;
7174 if ( this.items
[ i
].isSelected() !== selected
) {
7175 this.items
[ i
].setSelected( selected
);
7181 // TODO: When should a non-highlightable element be selected?
7182 if ( item
&& !item
.constructor.static.highlightable
) {
7184 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7186 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7189 this.emit( 'select', this.findSelectedItems() );
7198 * Press is a state that occurs when a user mouses down on an item, but has not
7199 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7200 * releases the mouse.
7202 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7205 * @return {OO.ui.Widget} The widget, for chaining
7207 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
7208 var i
, len
, pressed
,
7211 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7212 pressed
= this.items
[ i
] === item
;
7213 if ( this.items
[ i
].isPressed() !== pressed
) {
7214 this.items
[ i
].setPressed( pressed
);
7219 this.emit( 'press', item
);
7228 * Note that ‘choose’ should never be modified programmatically. A user can choose
7229 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7230 * use the #selectItem method.
7232 * This method is identical to #selectItem, but may vary in subclasses that take additional action
7233 * when users choose an item with the keyboard or mouse.
7235 * @param {OO.ui.OptionWidget} item Item to choose
7238 * @return {OO.ui.Widget} The widget, for chaining
7240 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
7242 if ( this.multiselect
&& item
.isSelected() ) {
7243 this.unselectItem( item
);
7245 this.selectItem( item
);
7248 this.emit( 'choose', item
, item
.isSelected() );
7255 * Find an option by its position relative to the specified item (or to the start of the option
7256 * array, if item is `null`). The direction in which to search through the option array is specified
7257 * with a number: -1 for reverse (the default) or 1 for forward. The method will return an option,
7258 * or `null` if there are no options in the array.
7260 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at
7261 * the beginning of the array.
7262 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7263 * @param {Function} [filter] Only consider items for which this function returns
7264 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
7265 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
7267 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
7268 var currentIndex
, nextIndex
, i
,
7269 increase
= direction
> 0 ? 1 : -1,
7270 len
= this.items
.length
;
7272 if ( item
instanceof OO
.ui
.OptionWidget
) {
7273 currentIndex
= this.items
.indexOf( item
);
7274 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7276 // If no item is selected and moving forward, start at the beginning.
7277 // If moving backward, start at the end.
7278 nextIndex
= direction
> 0 ? 0 : len
- 1;
7281 for ( i
= 0; i
< len
; i
++ ) {
7282 item
= this.items
[ nextIndex
];
7284 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7285 ( !filter
|| filter( item
) )
7289 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7295 * Find the next selectable item or `null` if there are no selectable items.
7296 * Disabled options and menu-section markers and breaks are not selectable.
7298 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7300 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7301 return this.findRelativeSelectableItem( null, 1 );
7305 * Add an array of options to the select. Optionally, an index number can be used to
7306 * specify an insertion point.
7308 * @param {OO.ui.OptionWidget[]} items Items to add
7309 * @param {number} [index] Index to insert items after
7312 * @return {OO.ui.Widget} The widget, for chaining
7314 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7316 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7318 // Always provide an index, even if it was omitted
7319 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7325 * Remove the specified array of options from the select. Options will be detached
7326 * from the DOM, not removed, so they can be reused later. To remove all options from
7327 * the select, you may wish to use the #clearItems method instead.
7329 * @param {OO.ui.OptionWidget[]} items Items to remove
7332 * @return {OO.ui.Widget} The widget, for chaining
7334 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7337 // Deselect items being removed
7338 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7340 if ( item
.isSelected() ) {
7341 this.selectItem( null );
7346 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7348 this.emit( 'remove', items
);
7354 * Clear all options from the select. Options will be detached from the DOM, not removed,
7355 * so that they can be reused later. To remove a subset of options from the select, use
7356 * the #removeItems method.
7360 * @return {OO.ui.Widget} The widget, for chaining
7362 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7363 var items
= this.items
.slice();
7366 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7369 this.selectItem( null );
7371 this.emit( 'remove', items
);
7377 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7379 * This is used to set `aria-activedescendant` and `aria-expanded` on it.
7382 * @param {jQuery} $focusOwner
7384 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7385 this.$focusOwner
= $focusOwner
;
7389 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7390 * with an {@link OO.ui.mixin.IconElement icon} and/or
7391 * {@link OO.ui.mixin.IndicatorElement indicator}.
7392 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7393 * options. For more information about options and selects, please see the
7394 * [OOUI documentation on MediaWiki][1].
7397 * // Decorated options in a select widget.
7398 * var select = new OO.ui.SelectWidget( {
7400 * new OO.ui.DecoratedOptionWidget( {
7402 * label: 'Option with icon',
7405 * new OO.ui.DecoratedOptionWidget( {
7407 * label: 'Option with indicator',
7412 * $( document.body ).append( select.$element );
7414 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7417 * @extends OO.ui.OptionWidget
7418 * @mixins OO.ui.mixin.IconElement
7419 * @mixins OO.ui.mixin.IndicatorElement
7422 * @param {Object} [config] Configuration options
7424 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7425 // Parent constructor
7426 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7428 // Mixin constructors
7429 OO
.ui
.mixin
.IconElement
.call( this, config
);
7430 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7434 .addClass( 'oo-ui-decoratedOptionWidget' )
7435 .prepend( this.$icon
)
7436 .append( this.$indicator
);
7441 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7442 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7443 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7446 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7447 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7448 * the [OOUI documentation on MediaWiki] [1] for more information.
7450 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7453 * @extends OO.ui.DecoratedOptionWidget
7456 * @param {Object} [config] Configuration options
7458 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7459 // Parent constructor
7460 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7463 this.checkIcon
= new OO
.ui
.IconWidget( {
7465 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7470 .prepend( this.checkIcon
.$element
)
7471 .addClass( 'oo-ui-menuOptionWidget' );
7476 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7478 /* Static Properties */
7484 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7487 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to
7488 * group one or more related {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets
7489 * cannot be highlighted or selected.
7492 * var dropdown = new OO.ui.DropdownWidget( {
7495 * new OO.ui.MenuSectionOptionWidget( {
7498 * new OO.ui.MenuOptionWidget( {
7500 * label: 'Welsh Corgi'
7502 * new OO.ui.MenuOptionWidget( {
7504 * label: 'Standard Poodle'
7506 * new OO.ui.MenuSectionOptionWidget( {
7509 * new OO.ui.MenuOptionWidget( {
7516 * $( document.body ).append( dropdown.$element );
7519 * @extends OO.ui.DecoratedOptionWidget
7522 * @param {Object} [config] Configuration options
7524 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7525 // Parent constructor
7526 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7530 .addClass( 'oo-ui-menuSectionOptionWidget' )
7531 .removeAttr( 'role aria-selected' );
7532 this.selected
= false;
7537 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7539 /* Static Properties */
7545 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7551 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7554 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7555 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7556 * See {@link OO.ui.DropdownWidget DropdownWidget},
7557 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}, and
7558 * {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7559 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7560 * and customized to be opened, closed, and displayed as needed.
7562 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7563 * mouse outside the menu.
7565 * Menus also have support for keyboard interaction:
7567 * - Enter/Return key: choose and select a menu option
7568 * - Up-arrow key: highlight the previous menu option
7569 * - Down-arrow key: highlight the next menu option
7570 * - Escape key: hide the menu
7572 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7574 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7575 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7578 * @extends OO.ui.SelectWidget
7579 * @mixins OO.ui.mixin.ClippableElement
7580 * @mixins OO.ui.mixin.FloatableElement
7583 * @param {Object} [config] Configuration options
7584 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu
7585 * items that match the text the user types. This config is used by
7586 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget} and
7587 * {@link OO.ui.mixin.LookupElement LookupElement}
7588 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7589 * the text the user types. This config is used by
7590 * {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
7591 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks
7592 * the mouse anywhere on the page outside of this widget, the menu is hidden. For example, if
7593 * there is a button that toggles the menu's visibility on click, the menu will be hidden then
7594 * re-shown when the user clicks that button, unless the button (or its parent widget) is passed
7596 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7597 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7598 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7599 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7600 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7601 * @cfg {string} [filterMode='prefix'] The mode by which the menu filters the results.
7602 * Options are 'exact', 'prefix' or 'substring'. See `OO.ui.SelectWidget#getItemMatcher`
7603 * @param {number|string} [width] Width of the menu as a number of pixels or CSS string with unit
7604 * suffix, used by {@link OO.ui.mixin.ClippableElement ClippableElement}
7606 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7607 // Configuration initialization
7608 config
= config
|| {};
7610 // Parent constructor
7611 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7613 // Mixin constructors
7614 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( { $clippable
: this.$group
}, config
) );
7615 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7617 // Initial vertical positions other than 'center' will result in
7618 // the menu being flipped if there is not enough space in the container.
7619 // Store the original position so we know what to reset to.
7620 this.originalVerticalPosition
= this.verticalPosition
;
7623 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7624 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7625 this.filterFromInput
= !!config
.filterFromInput
;
7626 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7627 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7628 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7629 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7630 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7631 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7632 this.lastHighlightedItem
= null;
7633 this.width
= config
.width
;
7634 this.filterMode
= config
.filterMode
;
7637 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7638 if ( config
.widget
) {
7639 this.setFocusOwner( config
.widget
.$tabIndexed
);
7642 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7643 // that reference properties not initialized at that time of parent class construction
7644 // TODO: Find a better way to handle post-constructor setup
7645 this.visible
= false;
7646 this.$element
.addClass( 'oo-ui-element-hidden' );
7647 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7652 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7653 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7654 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7661 * The menu is ready: it is visible and has been positioned and clipped.
7664 /* Static properties */
7667 * Positions to flip to if there isn't room in the container for the
7668 * menu in a specific direction.
7670 * @property {Object.<string,string>}
7672 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7682 * Handles document mouse down events.
7685 * @param {MouseEvent} e Mouse down event
7687 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7691 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7696 this.toggle( false );
7703 OO
.ui
.MenuSelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
7704 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7706 if ( !this.isDisabled() && this.isVisible() ) {
7707 switch ( e
.keyCode
) {
7708 case OO
.ui
.Keys
.LEFT
:
7709 case OO
.ui
.Keys
.RIGHT
:
7710 // Do nothing if a text field is associated, arrow keys will be handled natively
7711 if ( !this.$input
) {
7712 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7715 case OO
.ui
.Keys
.ESCAPE
:
7716 case OO
.ui
.Keys
.TAB
:
7717 if ( currentItem
&& !this.multiselect
) {
7718 currentItem
.setHighlighted( false );
7720 this.toggle( false );
7721 // Don't prevent tabbing away, prevent defocusing
7722 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7724 e
.stopPropagation();
7728 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7735 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7736 * or after items were added/removed (always).
7740 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7741 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7743 len
= this.items
.length
,
7744 showAll
= !this.isVisible(),
7747 if ( this.$input
&& this.filterFromInput
) {
7748 filter
= showAll
? null : this.getItemMatcher( this.$input
.val(), this.filterMode
);
7749 exactFilter
= this.getItemMatcher( this.$input
.val(), 'exact' );
7750 // Hide non-matching options, and also hide section headers if all options
7751 // in their section are hidden.
7752 for ( i
= 0; i
< len
; i
++ ) {
7753 item
= this.items
[ i
];
7754 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7756 // If the previous section was empty, hide its header
7757 section
.toggle( showAll
|| !sectionEmpty
);
7760 sectionEmpty
= true;
7761 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7762 visible
= showAll
|| filter( item
);
7763 exactMatch
= exactMatch
|| exactFilter( item
);
7764 anyVisible
= anyVisible
|| visible
;
7765 sectionEmpty
= sectionEmpty
&& !visible
;
7766 item
.toggle( visible
);
7769 // Process the final section
7771 section
.toggle( showAll
|| !sectionEmpty
);
7774 if ( !anyVisible
) {
7775 this.highlightItem( null );
7778 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7781 this.highlightOnFilter
&&
7782 !( this.lastHighlightedItem
&& this.lastHighlightedItem
.isVisible() )
7784 // Highlight the first item on the list
7786 items
= this.getItems();
7787 for ( i
= 0; i
< items
.length
; i
++ ) {
7788 if ( items
[ i
].isVisible() ) {
7793 this.highlightItem( item
);
7794 this.lastHighlightedItem
= item
;
7799 // Reevaluate clipping
7806 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyDownListener = function () {
7807 if ( this.$input
) {
7808 this.$input
.on( 'keydown', this.onDocumentKeyDownHandler
);
7810 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyDownListener
.call( this );
7817 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
7818 if ( this.$input
) {
7819 this.$input
.off( 'keydown', this.onDocumentKeyDownHandler
);
7821 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyDownListener
.call( this );
7828 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7829 if ( this.$input
) {
7830 if ( this.filterFromInput
) {
7832 'keydown mouseup cut paste change input select',
7833 this.onInputEditHandler
7835 this.updateItemVisibility();
7838 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyPressListener
.call( this );
7845 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7846 if ( this.$input
) {
7847 if ( this.filterFromInput
) {
7849 'keydown mouseup cut paste change input select',
7850 this.onInputEditHandler
7852 this.updateItemVisibility();
7855 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyPressListener
.call( this );
7862 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is
7865 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with
7866 * the keyboard or mouse and it becomes selected. To select an item programmatically,
7867 * use the #selectItem method.
7869 * @param {OO.ui.OptionWidget} item Item to choose
7871 * @return {OO.ui.Widget} The widget, for chaining
7873 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7874 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7875 if ( this.hideOnChoose
) {
7876 this.toggle( false );
7884 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7886 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7888 this.updateItemVisibility();
7896 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7898 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7900 this.updateItemVisibility();
7908 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7910 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7912 this.updateItemVisibility();
7918 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7919 * `.toggle( true )` after its #$element is attached to the DOM.
7921 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7922 * it in the right place and with the right dimensions only work correctly while it is attached.
7923 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7924 * strictly enforced, so currently it only generates a warning in the browser console.
7929 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7930 var change
, originalHeight
, flippedHeight
, selectedItem
;
7932 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7933 change
= visible
!== this.isVisible();
7935 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7936 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7937 this.warnedUnattached
= true;
7940 if ( change
&& visible
) {
7941 // Reset position before showing the popup again. It's possible we no longer need to flip
7942 // (e.g. if the user scrolled).
7943 this.setVerticalPosition( this.originalVerticalPosition
);
7947 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7953 this.setIdealSize( this.width
);
7954 } else if ( this.$floatableContainer
) {
7955 this.$clippable
.css( 'width', 'auto' );
7957 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
7958 // Dropdown is smaller than handle so expand to width
7959 this.$floatableContainer
[ 0 ].offsetWidth
:
7960 // Dropdown is larger than handle so auto size
7963 this.$clippable
.css( 'width', '' );
7966 this.togglePositioning( !!this.$floatableContainer
);
7967 this.toggleClipping( true );
7969 this.bindDocumentKeyDownListener();
7970 this.bindDocumentKeyPressListener();
7973 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
7974 this.originalVerticalPosition
!== 'center'
7976 // If opening the menu in one direction causes it to be clipped, flip it
7977 originalHeight
= this.$element
.height();
7978 this.setVerticalPosition(
7979 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
7981 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
7982 // If flipping also causes it to be clipped, open in whichever direction
7983 // we have more space
7984 flippedHeight
= this.$element
.height();
7985 if ( originalHeight
> flippedHeight
) {
7986 this.setVerticalPosition( this.originalVerticalPosition
);
7990 // Note that we do not flip the menu's opening direction if the clipping changes
7991 // later (e.g. after the user scrolls), that seems like it would be annoying
7993 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7995 selectedItem
= this.findSelectedItem();
7996 if ( !this.multiselect
&& selectedItem
) {
7997 // TODO: Verify if this is even needed; This is already done on highlight changes
7998 // in SelectWidget#highlightItem, so we should just need to highlight the item we need to
7999 // highlight here and not bother with attr or checking selections.
8000 this.$focusOwner
.attr( 'aria-activedescendant', selectedItem
.getElementId() );
8001 selectedItem
.scrollElementIntoView( { duration
: 0 } );
8005 if ( this.autoHide
) {
8006 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8009 this.emit( 'ready' );
8011 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
8012 this.unbindDocumentKeyDownListener();
8013 this.unbindDocumentKeyPressListener();
8014 this.$focusOwner
.attr( 'aria-expanded', 'false' );
8015 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8016 this.togglePositioning( false );
8017 this.toggleClipping( false );
8025 * Scroll to the top of the menu
8027 OO
.ui
.MenuSelectWidget
.prototype.scrollToTop = function () {
8028 this.$element
.scrollTop( 0 );
8032 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
8033 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
8034 * users can interact with it.
8036 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8037 * OO.ui.DropdownInputWidget instead.
8040 * // A DropdownWidget with a menu that contains three options.
8041 * var dropDown = new OO.ui.DropdownWidget( {
8042 * label: 'Dropdown menu: Select a menu option',
8045 * new OO.ui.MenuOptionWidget( {
8049 * new OO.ui.MenuOptionWidget( {
8053 * new OO.ui.MenuOptionWidget( {
8061 * $( document.body ).append( dropDown.$element );
8063 * dropDown.getMenu().selectItemByData( 'b' );
8065 * dropDown.getMenu().findSelectedItem().getData(); // Returns 'b'.
8067 * For more information, please see the [OOUI documentation on MediaWiki] [1].
8069 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8072 * @extends OO.ui.Widget
8073 * @mixins OO.ui.mixin.IconElement
8074 * @mixins OO.ui.mixin.IndicatorElement
8075 * @mixins OO.ui.mixin.LabelElement
8076 * @mixins OO.ui.mixin.TitledElement
8077 * @mixins OO.ui.mixin.TabIndexedElement
8080 * @param {Object} [config] Configuration options
8081 * @cfg {Object} [menu] Configuration options to pass to
8082 * {@link OO.ui.MenuSelectWidget menu select widget}.
8083 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
8084 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
8085 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
8086 * uses relative positioning.
8087 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
8089 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
8090 // Configuration initialization
8091 config
= $.extend( { indicator
: 'down' }, config
);
8093 // Parent constructor
8094 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
8096 // Properties (must be set before TabIndexedElement constructor call)
8097 this.$handle
= $( '<button>' );
8098 this.$overlay
= ( config
.$overlay
=== true ?
8099 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
8101 // Mixin constructors
8102 OO
.ui
.mixin
.IconElement
.call( this, config
);
8103 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8104 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8105 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
8106 $titled
: this.$label
8108 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
8109 $tabIndexed
: this.$handle
8113 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
8115 $floatableContainer
: this.$element
8120 click
: this.onClick
.bind( this ),
8121 keydown
: this.onKeyDown
.bind( this ),
8122 // Hack? Handle type-to-search when menu is not expanded and not handling its own events.
8123 keypress
: this.menu
.onDocumentKeyPressHandler
,
8124 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
8126 this.menu
.connect( this, {
8127 select
: 'onMenuSelect',
8128 toggle
: 'onMenuToggle'
8133 .addClass( 'oo-ui-dropdownWidget-handle' )
8136 'aria-owns': this.menu
.getElementId(),
8137 'aria-haspopup': 'listbox'
8139 .append( this.$icon
, this.$label
, this.$indicator
);
8141 .addClass( 'oo-ui-dropdownWidget' )
8142 .append( this.$handle
);
8143 this.$overlay
.append( this.menu
.$element
);
8148 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8149 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8150 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8151 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8152 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8153 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8160 * @return {OO.ui.MenuSelectWidget} Menu of widget
8162 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8167 * Handles menu select events.
8170 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8172 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8176 this.setLabel( null );
8180 selectedLabel
= item
.getLabel();
8182 // If the label is a DOM element, clone it, because setLabel will append() it
8183 if ( selectedLabel
instanceof $ ) {
8184 selectedLabel
= selectedLabel
.clone();
8187 this.setLabel( selectedLabel
);
8191 * Handle menu toggle events.
8194 * @param {boolean} isVisible Open state of the menu
8196 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8197 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8201 * Handle mouse click events.
8204 * @param {jQuery.Event} e Mouse click event
8205 * @return {undefined/boolean} False to prevent default if event is handled
8207 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8208 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8215 * Handle key down events.
8218 * @param {jQuery.Event} e Key down event
8219 * @return {undefined/boolean} False to prevent default if event is handled
8221 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8223 !this.isDisabled() &&
8225 e
.which
=== OO
.ui
.Keys
.ENTER
||
8227 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8228 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8229 // Space only closes the menu is the user is not typing to search.
8230 this.menu
.keyPressBuffer
=== ''
8233 !this.menu
.isVisible() &&
8235 e
.which
=== OO
.ui
.Keys
.UP
||
8236 e
.which
=== OO
.ui
.Keys
.DOWN
8247 * RadioOptionWidget is an option widget that looks like a radio button.
8248 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8249 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8251 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8254 * @extends OO.ui.OptionWidget
8257 * @param {Object} [config] Configuration options
8259 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8260 // Configuration initialization
8261 config
= config
|| {};
8263 // Properties (must be done before parent constructor which calls #setDisabled)
8264 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8266 // Parent constructor
8267 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8270 // Remove implicit role, we're handling it ourselves
8271 this.radio
.$input
.attr( 'role', 'presentation' );
8273 .addClass( 'oo-ui-radioOptionWidget' )
8274 .attr( 'role', 'radio' )
8275 .attr( 'aria-checked', 'false' )
8276 .removeAttr( 'aria-selected' )
8277 .prepend( this.radio
.$element
);
8282 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8284 /* Static Properties */
8290 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8296 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8302 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8308 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8315 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8316 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8318 this.radio
.setSelected( state
);
8320 .attr( 'aria-checked', state
.toString() )
8321 .removeAttr( 'aria-selected' );
8329 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8330 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8332 this.radio
.setDisabled( this.isDisabled() );
8338 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8339 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8340 * an interface for adding, removing and selecting options.
8341 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8343 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8344 * OO.ui.RadioSelectInputWidget instead.
8347 * // A RadioSelectWidget with RadioOptions.
8348 * var option1 = new OO.ui.RadioOptionWidget( {
8350 * label: 'Selected radio option'
8352 * option2 = new OO.ui.RadioOptionWidget( {
8354 * label: 'Unselected radio option'
8356 * radioSelect = new OO.ui.RadioSelectWidget( {
8357 * items: [ option1, option2 ]
8360 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8361 * radioSelect.selectItem( option1 );
8363 * $( document.body ).append( radioSelect.$element );
8365 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8369 * @extends OO.ui.SelectWidget
8370 * @mixins OO.ui.mixin.TabIndexedElement
8373 * @param {Object} [config] Configuration options
8375 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8376 // Parent constructor
8377 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8379 // Mixin constructors
8380 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8384 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8385 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8390 .addClass( 'oo-ui-radioSelectWidget' )
8391 .attr( 'role', 'radiogroup' );
8396 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8397 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8400 * MultioptionWidgets are special elements that can be selected and configured with data. The
8401 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8402 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8403 * and examples, please see the [OOUI documentation on MediaWiki][1].
8405 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8408 * @extends OO.ui.Widget
8409 * @mixins OO.ui.mixin.ItemWidget
8410 * @mixins OO.ui.mixin.LabelElement
8411 * @mixins OO.ui.mixin.TitledElement
8414 * @param {Object} [config] Configuration options
8415 * @cfg {boolean} [selected=false] Whether the option is initially selected
8417 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8418 // Configuration initialization
8419 config
= config
|| {};
8421 // Parent constructor
8422 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8424 // Mixin constructors
8425 OO
.ui
.mixin
.ItemWidget
.call( this );
8426 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8427 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8430 this.selected
= null;
8434 .addClass( 'oo-ui-multioptionWidget' )
8435 .append( this.$label
);
8436 this.setSelected( config
.selected
);
8441 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8442 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8443 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8444 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.TitledElement
);
8451 * A change event is emitted when the selected state of the option changes.
8453 * @param {boolean} selected Whether the option is now selected
8459 * Check if the option is selected.
8461 * @return {boolean} Item is selected
8463 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8464 return this.selected
;
8468 * Set the option’s selected state. In general, all modifications to the selection
8469 * should be handled by the SelectWidget’s
8470 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
8472 * @param {boolean} [state=false] Select option
8474 * @return {OO.ui.Widget} The widget, for chaining
8476 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8478 if ( this.selected
!== state
) {
8479 this.selected
= state
;
8480 this.emit( 'change', state
);
8481 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8487 * MultiselectWidget allows selecting multiple options from a list.
8489 * For more information about menus and options, please see the [OOUI documentation
8492 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8496 * @extends OO.ui.Widget
8497 * @mixins OO.ui.mixin.GroupWidget
8498 * @mixins OO.ui.mixin.TitledElement
8501 * @param {Object} [config] Configuration options
8502 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8504 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8505 // Parent constructor
8506 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8508 // Configuration initialization
8509 config
= config
|| {};
8511 // Mixin constructors
8512 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8513 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8519 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8520 // by GroupElement only when items are added/removed
8521 this.connect( this, {
8522 select
: [ 'emit', 'change' ]
8526 if ( config
.items
) {
8527 this.addItems( config
.items
);
8529 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8530 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8531 .append( this.$group
);
8536 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8537 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8538 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.TitledElement
);
8545 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8551 * A select event is emitted when an item is selected or deselected.
8557 * Find options that are selected.
8559 * @return {OO.ui.MultioptionWidget[]} Selected options
8561 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8562 return this.items
.filter( function ( item
) {
8563 return item
.isSelected();
8568 * Find the data of options that are selected.
8570 * @return {Object[]|string[]} Values of selected options
8572 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8573 return this.findSelectedItems().map( function ( item
) {
8579 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8581 * @param {OO.ui.MultioptionWidget[]} items Items to select
8583 * @return {OO.ui.Widget} The widget, for chaining
8585 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8586 this.items
.forEach( function ( item
) {
8587 var selected
= items
.indexOf( item
) !== -1;
8588 item
.setSelected( selected
);
8594 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8596 * @param {Object[]|string[]} datas Values of items to select
8598 * @return {OO.ui.Widget} The widget, for chaining
8600 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8603 items
= datas
.map( function ( data
) {
8604 return widget
.findItemFromData( data
);
8606 this.selectItems( items
);
8611 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8612 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8613 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8615 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8618 * @extends OO.ui.MultioptionWidget
8621 * @param {Object} [config] Configuration options
8623 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8624 // Configuration initialization
8625 config
= config
|| {};
8627 // Properties (must be done before parent constructor which calls #setDisabled)
8628 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8630 // Parent constructor
8631 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8634 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8635 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8639 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8640 .prepend( this.checkbox
.$element
);
8645 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8647 /* Static Properties */
8653 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8658 * Handle checkbox selected state change.
8662 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8663 this.setSelected( this.checkbox
.isSelected() );
8669 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8670 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8671 this.checkbox
.setSelected( state
);
8678 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8679 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8680 this.checkbox
.setDisabled( this.isDisabled() );
8687 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8688 this.checkbox
.focus();
8692 * Handle key down events.
8695 * @param {jQuery.Event} e
8697 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8699 element
= this.getElementGroup(),
8702 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8703 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8704 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8705 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8715 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8716 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8717 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8718 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8720 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8721 * OO.ui.CheckboxMultiselectInputWidget instead.
8724 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8725 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8728 * label: 'Selected checkbox'
8730 * option2 = new OO.ui.CheckboxMultioptionWidget( {
8732 * label: 'Unselected checkbox'
8734 * multiselect = new OO.ui.CheckboxMultiselectWidget( {
8735 * items: [ option1, option2 ]
8737 * $( document.body ).append( multiselect.$element );
8739 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8742 * @extends OO.ui.MultiselectWidget
8745 * @param {Object} [config] Configuration options
8747 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8748 // Parent constructor
8749 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8752 this.$lastClicked
= null;
8755 this.$group
.on( 'click', this.onClick
.bind( this ) );
8758 this.$element
.addClass( 'oo-ui-checkboxMultiselectWidget' );
8763 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8768 * Get an option by its position relative to the specified item (or to the start of the
8769 * option array, if item is `null`). The direction in which to search through the option array
8770 * is specified with a number: -1 for reverse (the default) or 1 for forward. The method will
8771 * return an option, or `null` if there are no options in the array.
8773 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or
8774 * `null` to start at the beginning of the array.
8775 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8776 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items
8779 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8780 var currentIndex
, nextIndex
, i
,
8781 increase
= direction
> 0 ? 1 : -1,
8782 len
= this.items
.length
;
8785 currentIndex
= this.items
.indexOf( item
);
8786 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8788 // If no item is selected and moving forward, start at the beginning.
8789 // If moving backward, start at the end.
8790 nextIndex
= direction
> 0 ? 0 : len
- 1;
8793 for ( i
= 0; i
< len
; i
++ ) {
8794 item
= this.items
[ nextIndex
];
8795 if ( item
&& !item
.isDisabled() ) {
8798 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8804 * Handle click events on checkboxes.
8806 * @param {jQuery.Event} e
8808 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8809 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8810 $lastClicked
= this.$lastClicked
,
8811 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8812 .not( '.oo-ui-widget-disabled' );
8814 // Allow selecting multiple options at once by Shift-clicking them
8815 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8816 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8817 lastClickedIndex
= $options
.index( $lastClicked
);
8818 nowClickedIndex
= $options
.index( $nowClicked
);
8819 // If it's the same item, either the user is being silly, or it's a fake event generated
8820 // by the browser. In either case we don't need custom handling.
8821 if ( nowClickedIndex
!== lastClickedIndex
) {
8823 wasSelected
= items
[ nowClickedIndex
].isSelected();
8824 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8826 // This depends on the DOM order of the items and the order of the .items array being
8828 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8829 if ( !items
[ i
].isDisabled() ) {
8830 items
[ i
].setSelected( !wasSelected
);
8833 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8834 // handling first, then set our value. The order in which events happen is different for
8835 // clicks on the <input> and on the <label> and there are additional fake clicks fired
8836 // for non-click actions that change the checkboxes.
8838 setTimeout( function () {
8839 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8840 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8846 if ( $nowClicked
.length
) {
8847 this.$lastClicked
= $nowClicked
;
8855 * @return {OO.ui.Widget} The widget, for chaining
8857 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8859 if ( !this.isDisabled() ) {
8860 item
= this.getRelativeFocusableItem( null, 1 );
8871 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8876 * Progress bars visually display the status of an operation, such as a download,
8877 * and can be either determinate or indeterminate:
8879 * - **determinate** process bars show the percent of an operation that is complete.
8881 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8882 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8883 * not use percentages.
8885 * The value of the `progress` configuration determines whether the bar is determinate
8889 * // Examples of determinate and indeterminate progress bars.
8890 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8893 * var progressBar2 = new OO.ui.ProgressBarWidget();
8895 * // Create a FieldsetLayout to layout progress bars.
8896 * var fieldset = new OO.ui.FieldsetLayout;
8897 * fieldset.addItems( [
8898 * new OO.ui.FieldLayout( progressBar1, {
8899 * label: 'Determinate',
8902 * new OO.ui.FieldLayout( progressBar2, {
8903 * label: 'Indeterminate',
8907 * $( document.body ).append( fieldset.$element );
8910 * @extends OO.ui.Widget
8913 * @param {Object} [config] Configuration options
8914 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8915 * To create a determinate progress bar, specify a number that reflects the initial
8917 * By default, the progress bar is indeterminate.
8919 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8920 // Configuration initialization
8921 config
= config
|| {};
8923 // Parent constructor
8924 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8927 this.$bar
= $( '<div>' );
8928 this.progress
= null;
8931 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8932 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8935 role
: 'progressbar',
8937 'aria-valuemax': 100
8939 .addClass( 'oo-ui-progressBarWidget' )
8940 .append( this.$bar
);
8945 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8947 /* Static Properties */
8953 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8958 * Get the percent of the progress that has been completed. Indeterminate progresses will
8961 * @return {number|boolean} Progress percent
8963 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8964 return this.progress
;
8968 * Set the percent of the process completed or `false` for an indeterminate process.
8970 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8972 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8973 this.progress
= progress
;
8975 if ( progress
!== false ) {
8976 this.$bar
.css( 'width', this.progress
+ '%' );
8977 this.$element
.attr( 'aria-valuenow', this.progress
);
8979 this.$bar
.css( 'width', '' );
8980 this.$element
.removeAttr( 'aria-valuenow' );
8982 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8986 * InputWidget is the base class for all input widgets, which
8987 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox
8988 * inputs}, {@link OO.ui.RadioInputWidget radio inputs}, and
8989 * {@link OO.ui.ButtonInputWidget button inputs}.
8990 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
8992 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
8996 * @extends OO.ui.Widget
8997 * @mixins OO.ui.mixin.TabIndexedElement
8998 * @mixins OO.ui.mixin.TitledElement
8999 * @mixins OO.ui.mixin.AccessKeyedElement
9002 * @param {Object} [config] Configuration options
9003 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9004 * @cfg {string} [value=''] The value of the input.
9005 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
9006 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
9007 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the
9008 * value of an input before it is accepted.
9010 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
9011 // Configuration initialization
9012 config
= config
|| {};
9014 // Parent constructor
9015 OO
.ui
.InputWidget
.parent
.call( this, config
);
9018 // See #reusePreInfuseDOM about config.$input
9019 this.$input
= config
.$input
|| this.getInputElement( config
);
9021 this.inputFilter
= config
.inputFilter
;
9023 // Mixin constructors
9024 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {
9025 $tabIndexed
: this.$input
9027 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {
9028 $titled
: this.$input
9030 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {
9031 $accessKeyed
: this.$input
9035 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
9039 .addClass( 'oo-ui-inputWidget-input' )
9040 .attr( 'name', config
.name
)
9041 .prop( 'disabled', this.isDisabled() );
9043 .addClass( 'oo-ui-inputWidget' )
9044 .append( this.$input
);
9045 this.setValue( config
.value
);
9047 this.setDir( config
.dir
);
9049 if ( config
.inputId
!== undefined ) {
9050 this.setInputId( config
.inputId
);
9056 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
9057 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
9058 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
9059 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
9061 /* Static Methods */
9066 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9067 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9068 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
9069 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
9076 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9077 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9078 if ( config
.$input
&& config
.$input
.length
) {
9079 state
.value
= config
.$input
.val();
9080 // Might be better in TabIndexedElement, but it's awkward to do there because
9081 // mixins are awkward
9082 state
.focus
= config
.$input
.is( ':focus' );
9092 * A change event is emitted when the value of the input changes.
9094 * @param {string} value
9100 * Get input element.
9102 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9103 * different circumstances. The element must have a `value` property (like form elements).
9106 * @param {Object} config Configuration options
9107 * @return {jQuery} Input element
9109 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
9110 return $( '<input>' );
9114 * Handle potentially value-changing events.
9117 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9119 OO
.ui
.InputWidget
.prototype.onEdit = function () {
9121 if ( !this.isDisabled() ) {
9122 // Allow the stack to clear so the value will be updated
9123 setTimeout( function () {
9124 widget
.setValue( widget
.$input
.val() );
9130 * Get the value of the input.
9132 * @return {string} Input value
9134 OO
.ui
.InputWidget
.prototype.getValue = function () {
9135 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9136 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9137 var value
= this.$input
.val();
9138 if ( this.value
!== value
) {
9139 this.setValue( value
);
9145 * Set the directionality of the input.
9147 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9149 * @return {OO.ui.Widget} The widget, for chaining
9151 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9152 this.$input
.prop( 'dir', dir
);
9157 * Set the value of the input.
9159 * @param {string} value New value
9162 * @return {OO.ui.Widget} The widget, for chaining
9164 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9165 value
= this.cleanUpValue( value
);
9166 // Update the DOM if it has changed. Note that with cleanUpValue, it
9167 // is possible for the DOM value to change without this.value changing.
9168 if ( this.$input
.val() !== value
) {
9169 this.$input
.val( value
);
9171 if ( this.value
!== value
) {
9173 this.emit( 'change', this.value
);
9175 // The first time that the value is set (probably while constructing the widget),
9176 // remember it in defaultValue. This property can be later used to check whether
9177 // the value of the input has been changed since it was created.
9178 if ( this.defaultValue
=== undefined ) {
9179 this.defaultValue
= this.value
;
9180 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9186 * Clean up incoming value.
9188 * Ensures value is a string, and converts undefined and null to empty string.
9191 * @param {string} value Original value
9192 * @return {string} Cleaned up value
9194 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9195 if ( value
=== undefined || value
=== null ) {
9197 } else if ( this.inputFilter
) {
9198 return this.inputFilter( String( value
) );
9200 return String( value
);
9207 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9208 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9209 if ( this.$input
) {
9210 this.$input
.prop( 'disabled', this.isDisabled() );
9216 * Set the 'id' attribute of the `<input>` element.
9218 * @param {string} id
9220 * @return {OO.ui.Widget} The widget, for chaining
9222 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9223 this.$input
.attr( 'id', id
);
9230 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9231 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9232 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9233 this.setValue( state
.value
);
9235 if ( state
.focus
) {
9241 * Data widget intended for creating `<input type="hidden">` inputs.
9244 * @extends OO.ui.Widget
9247 * @param {Object} [config] Configuration options
9248 * @cfg {string} [value=''] The value of the input.
9249 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9251 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9252 // Configuration initialization
9253 config
= $.extend( { value
: '', name
: '' }, config
);
9255 // Parent constructor
9256 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9259 this.$element
.attr( {
9261 value
: config
.value
,
9264 this.$element
.removeAttr( 'aria-disabled' );
9269 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9271 /* Static Properties */
9277 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9280 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9281 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9282 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9283 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9284 * [OOUI documentation on MediaWiki] [1] for more information.
9287 * // A ButtonInputWidget rendered as an HTML button, the default.
9288 * var button = new OO.ui.ButtonInputWidget( {
9289 * label: 'Input button',
9293 * $( document.body ).append( button.$element );
9295 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9298 * @extends OO.ui.InputWidget
9299 * @mixins OO.ui.mixin.ButtonElement
9300 * @mixins OO.ui.mixin.IconElement
9301 * @mixins OO.ui.mixin.IndicatorElement
9302 * @mixins OO.ui.mixin.LabelElement
9303 * @mixins OO.ui.mixin.FlaggedElement
9306 * @param {Object} [config] Configuration options
9307 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute:
9308 * 'button', 'submit' or 'reset'.
9309 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9310 * Widgets configured to be an `<input>` do not support {@link #icon icons} and
9311 * {@link #indicator indicators},
9312 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should
9313 * only be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9315 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9316 // Configuration initialization
9317 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9319 // See InputWidget#reusePreInfuseDOM about config.$input
9320 if ( config
.$input
) {
9321 config
.$input
.empty();
9324 // Properties (must be set before parent constructor, which calls #setValue)
9325 this.useInputTag
= config
.useInputTag
;
9327 // Parent constructor
9328 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9330 // Mixin constructors
9331 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {
9332 $button
: this.$input
9334 OO
.ui
.mixin
.IconElement
.call( this, config
);
9335 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9336 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9337 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
9340 if ( !config
.useInputTag
) {
9341 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9343 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9348 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9349 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9350 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9351 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9352 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9353 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
9355 /* Static Properties */
9361 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9369 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9371 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9372 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9378 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9380 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9381 * text, or `null` for no label
9383 * @return {OO.ui.Widget} The widget, for chaining
9385 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9386 if ( typeof label
=== 'function' ) {
9387 label
= OO
.ui
.resolveMsg( label
);
9390 if ( this.useInputTag
) {
9391 // Discard non-plaintext labels
9392 if ( typeof label
!== 'string' ) {
9396 this.$input
.val( label
);
9399 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9403 * Set the value of the input.
9405 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9406 * they do not support {@link #value values}.
9408 * @param {string} value New value
9410 * @return {OO.ui.Widget} The widget, for chaining
9412 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9413 if ( !this.useInputTag
) {
9414 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9422 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9423 // Disable generating `<label>` elements for buttons. One would very rarely need additional
9424 // label for a button, and it's already a big clickable target, and it causes
9425 // unexpected rendering.
9430 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9431 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9432 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9433 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9435 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9438 * // An example of selected, unselected, and disabled checkbox inputs.
9439 * var checkbox1 = new OO.ui.CheckboxInputWidget( {
9443 * checkbox2 = new OO.ui.CheckboxInputWidget( {
9446 * checkbox3 = new OO.ui.CheckboxInputWidget( {
9450 * // Create a fieldset layout with fields for each checkbox.
9451 * fieldset = new OO.ui.FieldsetLayout( {
9452 * label: 'Checkboxes'
9454 * fieldset.addItems( [
9455 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9456 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9457 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9459 * $( document.body ).append( fieldset.$element );
9461 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9464 * @extends OO.ui.InputWidget
9467 * @param {Object} [config] Configuration options
9468 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is
9470 * @cfg {boolean} [indeterminate=false] Whether the checkbox is in the indeterminate state.
9472 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9473 // Configuration initialization
9474 config
= config
|| {};
9476 // Parent constructor
9477 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9480 this.checkIcon
= new OO
.ui
.IconWidget( {
9482 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9487 .addClass( 'oo-ui-checkboxInputWidget' )
9488 // Required for pretty styling in WikimediaUI theme
9489 .append( this.checkIcon
.$element
);
9490 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9491 this.setIndeterminate( config
.indeterminate
!== undefined ? config
.indeterminate
: false );
9496 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9503 * A change event is emitted when the state of the input changes.
9505 * @param {boolean} selected
9506 * @param {boolean} indeterminate
9509 /* Static Properties */
9515 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9517 /* Static Methods */
9522 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9523 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9524 state
.checked
= config
.$input
.prop( 'checked' );
9534 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9535 return $( '<input>' ).attr( 'type', 'checkbox' );
9541 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9543 if ( !this.isDisabled() ) {
9544 // Allow the stack to clear so the value will be updated
9545 setTimeout( function () {
9546 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9547 widget
.setIndeterminate( widget
.$input
.prop( 'indeterminate' ) );
9553 * Set selection state of this checkbox.
9555 * @param {boolean} state Selected state
9556 * @param {boolean} internal Used for internal calls to suppress events
9558 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9560 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
, internal ) {
9562 if ( this.selected
!== state
) {
9563 this.selected
= state
;
9564 this.$input
.prop( 'checked', this.selected
);
9566 this.setIndeterminate( false, true );
9567 this.emit( 'change', this.selected
, this.indeterminate
);
9570 // The first time that the selection state is set (probably while constructing the widget),
9571 // remember it in defaultSelected. This property can be later used to check whether
9572 // the selection state of the input has been changed since it was created.
9573 if ( this.defaultSelected
=== undefined ) {
9574 this.defaultSelected
= this.selected
;
9575 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9581 * Check if this checkbox is selected.
9583 * @return {boolean} Checkbox is selected
9585 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9586 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9587 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9588 var selected
= this.$input
.prop( 'checked' );
9589 if ( this.selected
!== selected
) {
9590 this.setSelected( selected
);
9592 return this.selected
;
9596 * Set indeterminate state of this checkbox.
9598 * @param {boolean} state Indeterminate state
9599 * @param {boolean} internal Used for internal calls to suppress events
9601 * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
9603 OO
.ui
.CheckboxInputWidget
.prototype.setIndeterminate = function ( state
, internal ) {
9605 if ( this.indeterminate
!== state
) {
9606 this.indeterminate
= state
;
9607 this.$input
.prop( 'indeterminate', this.indeterminate
);
9609 this.setSelected( false, true );
9610 this.emit( 'change', this.selected
, this.indeterminate
);
9617 * Check if this checkbox is selected.
9619 * @return {boolean} Checkbox is selected
9621 OO
.ui
.CheckboxInputWidget
.prototype.isIndeterminate = function () {
9622 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9623 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9624 var indeterminate
= this.$input
.prop( 'indeterminate' );
9625 if ( this.indeterminate
!== indeterminate
) {
9626 this.setIndeterminate( indeterminate
);
9628 return this.indeterminate
;
9634 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9635 if ( !this.isDisabled() ) {
9636 this.$handle
.trigger( 'click' );
9644 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9645 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9646 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9647 this.setSelected( state
.checked
);
9652 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9653 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the
9654 * value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9655 * more information about input widgets.
9657 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9658 * are no options. If no `value` configuration option is provided, the first option is selected.
9659 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9661 * This and OO.ui.RadioSelectInputWidget support similar configuration options.
9664 * // A DropdownInputWidget with three options.
9665 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9667 * { data: 'a', label: 'First' },
9668 * { data: 'b', label: 'Second', disabled: true },
9669 * { optgroup: 'Group label' },
9670 * { data: 'c', label: 'First sub-item)' }
9673 * $( document.body ).append( dropdownInput.$element );
9675 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9678 * @extends OO.ui.InputWidget
9681 * @param {Object} [config] Configuration options
9682 * @cfg {Object[]} [options=[]] Array of menu options in the format described above.
9683 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9684 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
9685 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
9686 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
9687 * uses relative positioning.
9688 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9690 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9691 // Configuration initialization
9692 config
= config
|| {};
9694 // Properties (must be done before parent constructor which calls #setDisabled)
9695 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9697 $overlay
: config
.$overlay
9701 // Set up the options before parent constructor, which uses them to validate config.value.
9702 // Use this instead of setOptions() because this.$input is not set up yet.
9703 this.setOptionsData( config
.options
|| [] );
9705 // Parent constructor
9706 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9709 this.dropdownWidget
.getMenu().connect( this, {
9710 select
: 'onMenuSelect'
9715 .addClass( 'oo-ui-dropdownInputWidget' )
9716 .append( this.dropdownWidget
.$element
);
9717 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9718 this.setTitledElement( this.dropdownWidget
.$handle
);
9723 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9731 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9732 return $( '<select>' );
9736 * Handles menu select events.
9739 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9741 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9742 this.setValue( item
? item
.getData() : '' );
9748 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9750 value
= this.cleanUpValue( value
);
9751 // Only allow setting values that are actually present in the dropdown
9752 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9753 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9754 this.dropdownWidget
.getMenu().selectItem( selected
);
9755 value
= selected
? selected
.getData() : '';
9756 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9757 if ( this.optionsDirty
) {
9758 // We reached this from the constructor or from #setOptions.
9759 // We have to update the <select> element.
9760 this.updateOptionsInterface();
9768 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9769 this.dropdownWidget
.setDisabled( state
);
9770 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9775 * Set the options available for this input.
9777 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9779 * @return {OO.ui.Widget} The widget, for chaining
9781 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9782 var value
= this.getValue();
9784 this.setOptionsData( options
);
9786 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9787 // In case the previous value is no longer an available option, select the first valid one.
9788 this.setValue( value
);
9794 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9796 * This method may be called before the parent constructor, so various properties may not be
9799 * @param {Object[]} options Array of menu options (see #constructor for details).
9802 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9803 var optionWidgets
, optIndex
, opt
, previousOptgroup
, optionWidget
, optValue
,
9806 this.optionsDirty
= true;
9808 // Go through all the supplied option configs and create either
9809 // MenuSectionOption or MenuOption widgets from each.
9811 for ( optIndex
= 0; optIndex
< options
.length
; optIndex
++ ) {
9812 opt
= options
[ optIndex
];
9814 if ( opt
.optgroup
!== undefined ) {
9815 // Create a <optgroup> menu item.
9816 optionWidget
= widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9817 previousOptgroup
= optionWidget
;
9820 // Create a normal <option> menu item.
9821 optValue
= widget
.cleanUpValue( opt
.data
);
9822 optionWidget
= widget
.createMenuOptionWidget(
9824 opt
.label
!== undefined ? opt
.label
: optValue
9828 // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
9830 opt
.disabled
!== undefined ||
9831 previousOptgroup
instanceof OO
.ui
.MenuSectionOptionWidget
&&
9832 previousOptgroup
.isDisabled()
9834 optionWidget
.setDisabled( true );
9837 optionWidgets
.push( optionWidget
);
9840 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9844 * Create a menu option widget.
9847 * @param {string} data Item data
9848 * @param {string} label Item label
9849 * @return {OO.ui.MenuOptionWidget} Option widget
9851 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9852 return new OO
.ui
.MenuOptionWidget( {
9859 * Create a menu section option widget.
9862 * @param {string} label Section item label
9863 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9865 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9866 return new OO
.ui
.MenuSectionOptionWidget( {
9872 * Update the user-visible interface to match the internal list of options and value.
9874 * This method must only be called after the parent constructor.
9878 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9880 $optionsContainer
= this.$input
,
9881 defaultValue
= this.defaultValue
,
9884 this.$input
.empty();
9886 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9889 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9890 $optionNode
= $( '<option>' )
9891 .attr( 'value', optionWidget
.getData() )
9892 .text( optionWidget
.getLabel() );
9894 // Remember original selection state. This property can be later used to check whether
9895 // the selection state of the input has been changed since it was created.
9896 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9898 $optionsContainer
.append( $optionNode
);
9900 $optionNode
= $( '<optgroup>' )
9901 .attr( 'label', optionWidget
.getLabel() );
9902 widget
.$input
.append( $optionNode
);
9903 $optionsContainer
= $optionNode
;
9906 // Disable the option or optgroup if required.
9907 if ( optionWidget
.isDisabled() ) {
9908 $optionNode
.prop( 'disabled', true );
9912 this.optionsDirty
= false;
9918 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9919 this.dropdownWidget
.focus();
9926 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9927 this.dropdownWidget
.blur();
9932 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9933 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9934 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9935 * please see the [OOUI documentation on MediaWiki][1].
9937 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9940 * // An example of selected, unselected, and disabled radio inputs
9941 * var radio1 = new OO.ui.RadioInputWidget( {
9945 * var radio2 = new OO.ui.RadioInputWidget( {
9948 * var radio3 = new OO.ui.RadioInputWidget( {
9952 * // Create a fieldset layout with fields for each radio button.
9953 * var fieldset = new OO.ui.FieldsetLayout( {
9954 * label: 'Radio inputs'
9956 * fieldset.addItems( [
9957 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9958 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9959 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9961 * $( document.body ).append( fieldset.$element );
9963 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9966 * @extends OO.ui.InputWidget
9969 * @param {Object} [config] Configuration options
9970 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button
9973 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9974 // Configuration initialization
9975 config
= config
|| {};
9977 // Parent constructor
9978 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9982 .addClass( 'oo-ui-radioInputWidget' )
9983 // Required for pretty styling in WikimediaUI theme
9984 .append( $( '<span>' ) );
9985 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9990 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9992 /* Static Properties */
9998 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
10000 /* Static Methods */
10005 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10006 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10007 state
.checked
= config
.$input
.prop( 'checked' );
10017 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
10018 return $( '<input>' ).attr( 'type', 'radio' );
10024 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
10025 // RadioInputWidget doesn't track its state.
10029 * Set selection state of this radio button.
10031 * @param {boolean} state `true` for selected
10033 * @return {OO.ui.Widget} The widget, for chaining
10035 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
10036 // RadioInputWidget doesn't track its state.
10037 this.$input
.prop( 'checked', state
);
10038 // The first time that the selection state is set (probably while constructing the widget),
10039 // remember it in defaultSelected. This property can be later used to check whether
10040 // the selection state of the input has been changed since it was created.
10041 if ( this.defaultSelected
=== undefined ) {
10042 this.defaultSelected
= state
;
10043 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
10049 * Check if this radio button is selected.
10051 * @return {boolean} Radio is selected
10053 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
10054 return this.$input
.prop( 'checked' );
10060 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
10061 if ( !this.isDisabled() ) {
10062 this.$input
.trigger( 'click' );
10070 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10071 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10072 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
10073 this.setSelected( state
.checked
);
10078 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be
10079 * used within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with
10080 * the value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
10081 * more information about input widgets.
10083 * This and OO.ui.DropdownInputWidget support similar configuration options.
10086 * // A RadioSelectInputWidget with three options
10087 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
10089 * { data: 'a', label: 'First' },
10090 * { data: 'b', label: 'Second'},
10091 * { data: 'c', label: 'Third' }
10094 * $( document.body ).append( radioSelectInput.$element );
10096 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10099 * @extends OO.ui.InputWidget
10102 * @param {Object} [config] Configuration options
10103 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10105 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
10106 // Configuration initialization
10107 config
= config
|| {};
10109 // Properties (must be done before parent constructor which calls #setDisabled)
10110 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
10111 // Set up the options before parent constructor, which uses them to validate config.value.
10112 // Use this instead of setOptions() because this.$input is not set up yet
10113 this.setOptionsData( config
.options
|| [] );
10115 // Parent constructor
10116 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
10119 this.radioSelectWidget
.connect( this, {
10120 select
: 'onMenuSelect'
10125 .addClass( 'oo-ui-radioSelectInputWidget' )
10126 .append( this.radioSelectWidget
.$element
);
10127 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
10132 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
10134 /* Static Methods */
10139 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10140 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10141 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
10148 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10149 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10150 // Cannot reuse the `<input type=radio>` set
10151 delete config
.$input
;
10161 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
10162 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
10163 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
10164 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
10168 * Handles menu select events.
10171 * @param {OO.ui.RadioOptionWidget} item Selected menu item
10173 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
10174 this.setValue( item
.getData() );
10180 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
10182 value
= this.cleanUpValue( value
);
10183 // Only allow setting values that are actually present in the dropdown
10184 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
10185 this.radioSelectWidget
.findFirstSelectableItem();
10186 this.radioSelectWidget
.selectItem( selected
);
10187 value
= selected
? selected
.getData() : '';
10188 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10195 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10196 this.radioSelectWidget
.setDisabled( state
);
10197 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10202 * Set the options available for this input.
10204 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10206 * @return {OO.ui.Widget} The widget, for chaining
10208 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10209 var value
= this.getValue();
10211 this.setOptionsData( options
);
10213 // Re-set the value to update the visible interface (RadioSelectWidget).
10214 // In case the previous value is no longer an available option, select the first valid one.
10215 this.setValue( value
);
10221 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10223 * This method may be called before the parent constructor, so various properties may not be
10226 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10229 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10232 this.radioSelectWidget
10234 .addItems( options
.map( function ( opt
) {
10235 var optValue
= widget
.cleanUpValue( opt
.data
);
10236 return new OO
.ui
.RadioOptionWidget( {
10238 label
: opt
.label
!== undefined ? opt
.label
: optValue
10246 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10247 this.radioSelectWidget
.focus();
10254 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10255 this.radioSelectWidget
.blur();
10260 * CheckboxMultiselectInputWidget is a
10261 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10262 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10263 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10264 * more information about input widgets.
10267 * // A CheckboxMultiselectInputWidget with three options.
10268 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10270 * { data: 'a', label: 'First' },
10271 * { data: 'b', label: 'Second' },
10272 * { data: 'c', label: 'Third' }
10275 * $( document.body ).append( multiselectInput.$element );
10277 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10280 * @extends OO.ui.InputWidget
10283 * @param {Object} [config] Configuration options
10284 * @cfg {Object[]} [options=[]] Array of menu options in the format
10285 * `{ data: …, label: …, disabled: … }`
10287 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10288 // Configuration initialization
10289 config
= config
|| {};
10291 // Properties (must be done before parent constructor which calls #setDisabled)
10292 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10293 // Must be set before the #setOptionsData call below
10294 this.inputName
= config
.name
;
10295 // Set up the options before parent constructor, which uses them to validate config.value.
10296 // Use this instead of setOptions() because this.$input is not set up yet
10297 this.setOptionsData( config
.options
|| [] );
10299 // Parent constructor
10300 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10303 this.checkboxMultiselectWidget
.connect( this, {
10304 select
: 'onCheckboxesSelect'
10309 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10310 .append( this.checkboxMultiselectWidget
.$element
);
10311 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10312 this.$input
.detach();
10317 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10319 /* Static Methods */
10324 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10325 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState(
10328 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10329 .toArray().map( function ( el
) { return el
.value
; } );
10336 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10337 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10338 // Cannot reuse the `<input type=checkbox>` set
10339 delete config
.$input
;
10349 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10351 return $( '<unused>' );
10355 * Handles CheckboxMultiselectWidget select events.
10359 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10360 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10366 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10367 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10368 .toArray().map( function ( el
) { return el
.value
; } );
10369 if ( this.value
!== value
) {
10370 this.setValue( value
);
10378 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10379 value
= this.cleanUpValue( value
);
10380 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10381 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10382 if ( this.optionsDirty
) {
10383 // We reached this from the constructor or from #setOptions.
10384 // We have to update the <select> element.
10385 this.updateOptionsInterface();
10391 * Clean up incoming value.
10393 * @param {string[]} value Original value
10394 * @return {string[]} Cleaned up value
10396 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10397 var i
, singleValue
,
10399 if ( !Array
.isArray( value
) ) {
10402 for ( i
= 0; i
< value
.length
; i
++ ) {
10403 singleValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10404 .call( this, value
[ i
] );
10405 // Remove options that we don't have here
10406 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10409 cleanValue
.push( singleValue
);
10417 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10418 this.checkboxMultiselectWidget
.setDisabled( state
);
10419 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10424 * Set the options available for this input.
10426 * @param {Object[]} options Array of menu options in the format
10427 * `{ data: …, label: …, disabled: … }`
10429 * @return {OO.ui.Widget} The widget, for chaining
10431 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10432 var value
= this.getValue();
10434 this.setOptionsData( options
);
10436 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10437 // This will also get rid of any stale options that we just removed.
10438 this.setValue( value
);
10444 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10446 * This method may be called before the parent constructor, so various properties may not be
10449 * @param {Object[]} options Array of menu options in the format
10450 * `{ data: …, label: … }`
10453 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10456 this.optionsDirty
= true;
10458 this.checkboxMultiselectWidget
10460 .addItems( options
.map( function ( opt
) {
10461 var optValue
, item
, optDisabled
;
10462 optValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10463 .call( widget
, opt
.data
);
10464 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10465 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10467 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10468 disabled
: optDisabled
10470 // Set the 'name' and 'value' for form submission
10471 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10472 item
.checkbox
.setValue( optValue
);
10478 * Update the user-visible interface to match the internal list of options and value.
10480 * This method must only be called after the parent constructor.
10484 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10485 var defaultValue
= this.defaultValue
;
10487 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10488 // Remember original selection state. This property can be later used to check whether
10489 // the selection state of the input has been changed since it was created.
10490 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10491 item
.checkbox
.defaultSelected
= isDefault
;
10492 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10495 this.optionsDirty
= false;
10501 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10502 this.checkboxMultiselectWidget
.focus();
10507 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10508 * size of the field as well as its presentation. In addition, these widgets can be configured
10509 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an
10510 * optional validation-pattern (used to determine if an input value is valid or not) and an input
10511 * filter, which modifies incoming values rather than validating them.
10512 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10514 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10517 * // A TextInputWidget.
10518 * var textInput = new OO.ui.TextInputWidget( {
10519 * value: 'Text input'
10521 * $( document.body ).append( textInput.$element );
10523 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10526 * @extends OO.ui.InputWidget
10527 * @mixins OO.ui.mixin.IconElement
10528 * @mixins OO.ui.mixin.IndicatorElement
10529 * @mixins OO.ui.mixin.PendingElement
10530 * @mixins OO.ui.mixin.LabelElement
10531 * @mixins OO.ui.mixin.FlaggedElement
10534 * @param {Object} [config] Configuration options
10535 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10536 * 'email', 'url' or 'number'.
10537 * @cfg {string} [placeholder] Placeholder text
10538 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10539 * instruct the browser to focus this widget.
10540 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10541 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10543 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10544 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10545 * many emojis) count as 2 characters each.
10546 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10547 * the value or placeholder text: `'before'` or `'after'`
10548 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator:
10549 * 'required'`. Note that `false` & setting `indicator: 'required' will result in no indicator
10551 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10552 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined`
10553 * means leaving it up to the browser).
10554 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10555 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10556 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10557 * value for it to be considered valid; when Function, a function receiving the value as parameter
10558 * that must return true, or promise resolving to true, for it to be considered valid.
10560 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10561 // Configuration initialization
10562 config
= $.extend( {
10564 labelPosition
: 'after'
10567 // Parent constructor
10568 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10570 // Mixin constructors
10571 OO
.ui
.mixin
.IconElement
.call( this, config
);
10572 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10573 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( { $pending
: this.$input
}, config
) );
10574 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10575 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
10578 this.type
= this.getSaneType( config
);
10579 this.readOnly
= false;
10580 this.required
= false;
10581 this.validate
= null;
10582 this.scrollWidth
= null;
10584 this.setValidation( config
.validate
);
10585 this.setLabelPosition( config
.labelPosition
);
10589 keypress
: this.onKeyPress
.bind( this ),
10590 blur
: this.onBlur
.bind( this ),
10591 focus
: this.onFocus
.bind( this )
10593 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10594 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10595 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10596 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10600 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10601 .append( this.$icon
, this.$indicator
);
10602 this.setReadOnly( !!config
.readOnly
);
10603 this.setRequired( !!config
.required
);
10604 if ( config
.placeholder
!== undefined ) {
10605 this.$input
.attr( 'placeholder', config
.placeholder
);
10607 if ( config
.maxLength
!== undefined ) {
10608 this.$input
.attr( 'maxlength', config
.maxLength
);
10610 if ( config
.autofocus
) {
10611 this.$input
.attr( 'autofocus', 'autofocus' );
10613 if ( config
.autocomplete
=== false ) {
10614 this.$input
.attr( 'autocomplete', 'off' );
10615 // Turning off autocompletion also disables "form caching" when the user navigates to a
10616 // different page and then clicks "Back". Re-enable it when leaving.
10617 // Borrowed from jQuery UI.
10619 beforeunload: function () {
10620 this.$input
.removeAttr( 'autocomplete' );
10622 pageshow: function () {
10623 // Browsers don't seem to actually fire this event on "Back", they instead just
10624 // reload the whole page... it shouldn't hurt, though.
10625 this.$input
.attr( 'autocomplete', 'off' );
10629 if ( config
.spellcheck
!== undefined ) {
10630 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10632 if ( this.label
) {
10633 this.isWaitingToBeAttached
= true;
10634 this.installParentChangeDetector();
10640 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10641 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10642 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10643 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10644 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10645 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
10647 /* Static Properties */
10649 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10657 * An `enter` event is emitted when the user presses Enter key inside the text box.
10665 * Handle icon mouse down events.
10668 * @param {jQuery.Event} e Mouse down event
10669 * @return {undefined/boolean} False to prevent default if event is handled
10671 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10672 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10679 * Handle indicator mouse down events.
10682 * @param {jQuery.Event} e Mouse down event
10683 * @return {undefined/boolean} False to prevent default if event is handled
10685 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10686 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10693 * Handle key press events.
10696 * @param {jQuery.Event} e Key press event
10697 * @fires enter If Enter key is pressed
10699 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10700 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10701 this.emit( 'enter', e
);
10706 * Handle blur events.
10709 * @param {jQuery.Event} e Blur event
10711 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10712 this.setValidityFlag();
10716 * Handle focus events.
10719 * @param {jQuery.Event} e Focus event
10721 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10722 if ( this.isWaitingToBeAttached
) {
10723 // If we've received focus, then we must be attached to the document, and if
10724 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10725 this.onElementAttach();
10727 this.setValidityFlag( true );
10731 * Handle element attach events.
10734 * @param {jQuery.Event} e Element attach event
10736 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10737 this.isWaitingToBeAttached
= false;
10738 // Any previously calculated size is now probably invalid if we reattached elsewhere
10739 this.valCache
= null;
10740 this.positionLabel();
10744 * Handle debounced change events.
10746 * @param {string} value
10749 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10750 this.setValidityFlag();
10754 * Check if the input is {@link #readOnly read-only}.
10756 * @return {boolean}
10758 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10759 return this.readOnly
;
10763 * Set the {@link #readOnly read-only} state of the input.
10765 * @param {boolean} state Make input read-only
10767 * @return {OO.ui.Widget} The widget, for chaining
10769 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10770 this.readOnly
= !!state
;
10771 this.$input
.prop( 'readOnly', this.readOnly
);
10776 * Check if the input is {@link #required required}.
10778 * @return {boolean}
10780 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10781 return this.required
;
10785 * Set the {@link #required required} state of the input.
10787 * @param {boolean} state Make input required
10789 * @return {OO.ui.Widget} The widget, for chaining
10791 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10792 this.required
= !!state
;
10793 if ( this.required
) {
10795 .prop( 'required', true )
10796 .attr( 'aria-required', 'true' );
10797 if ( this.getIndicator() === null ) {
10798 this.setIndicator( 'required' );
10802 .prop( 'required', false )
10803 .removeAttr( 'aria-required' );
10804 if ( this.getIndicator() === 'required' ) {
10805 this.setIndicator( null );
10812 * Support function for making #onElementAttach work across browsers.
10814 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10815 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10817 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10818 * first time that the element gets attached to the documented.
10820 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10821 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10822 MutationObserver
= window
.MutationObserver
||
10823 window
.WebKitMutationObserver
||
10824 window
.MozMutationObserver
,
10827 if ( MutationObserver
) {
10828 // The new way. If only it wasn't so ugly.
10830 if ( this.isElementAttached() ) {
10831 // Widget is attached already, do nothing. This breaks the functionality of this
10832 // function when the widget is detached and reattached. Alas, doing this correctly with
10833 // MutationObserver would require observation of the whole document, which would hurt
10834 // performance of other, more important code.
10838 // Find topmost node in the tree
10839 topmostNode
= this.$element
[ 0 ];
10840 while ( topmostNode
.parentNode
) {
10841 topmostNode
= topmostNode
.parentNode
;
10844 // We have no way to detect the $element being attached somewhere without observing the
10845 // entire DOM with subtree modifications, which would hurt performance. So we cheat: we hook
10846 // to the parent node of $element, and instead detect when $element is removed from it (and
10847 // thus probably attached somewhere else). If there is no parent, we create a "fake" one. If
10848 // it doesn't get attached, we end up back here and create the parent.
10849 mutationObserver
= new MutationObserver( function ( mutations
) {
10850 var i
, j
, removedNodes
;
10851 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10852 removedNodes
= mutations
[ i
].removedNodes
;
10853 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10854 if ( removedNodes
[ j
] === topmostNode
) {
10855 setTimeout( onRemove
, 0 );
10862 onRemove = function () {
10863 // If the node was attached somewhere else, report it
10864 if ( widget
.isElementAttached() ) {
10865 widget
.onElementAttach();
10867 mutationObserver
.disconnect();
10868 widget
.installParentChangeDetector();
10871 // Create a fake parent and observe it
10872 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10873 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10875 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10876 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10877 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10885 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10886 if ( this.getSaneType( config
) === 'number' ) {
10887 return $( '<input>' )
10888 .attr( 'step', 'any' )
10889 .attr( 'type', 'number' );
10891 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10896 * Get sanitized value for 'type' for given config.
10898 * @param {Object} config Configuration options
10899 * @return {string|null}
10902 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10903 var allowedTypes
= [
10910 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10914 * Focus the input and select a specified range within the text.
10916 * @param {number} from Select from offset
10917 * @param {number} [to] Select to offset, defaults to from
10919 * @return {OO.ui.Widget} The widget, for chaining
10921 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10922 var isBackwards
, start
, end
,
10923 input
= this.$input
[ 0 ];
10927 isBackwards
= to
< from;
10928 start
= isBackwards
? to
: from;
10929 end
= isBackwards
? from : to
;
10934 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10936 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10937 // Rather than expensively check if the input is attached every time, just check
10938 // if it was the cause of an error being thrown. If not, rethrow the error.
10939 if ( this.getElementDocument().body
.contains( input
) ) {
10947 * Get an object describing the current selection range in a directional manner
10949 * @return {Object} Object containing 'from' and 'to' offsets
10951 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10952 var input
= this.$input
[ 0 ],
10953 start
= input
.selectionStart
,
10954 end
= input
.selectionEnd
,
10955 isBackwards
= input
.selectionDirection
=== 'backward';
10958 from: isBackwards
? end
: start
,
10959 to
: isBackwards
? start
: end
10964 * Get the length of the text input value.
10966 * This could differ from the length of #getValue if the
10967 * value gets filtered
10969 * @return {number} Input length
10971 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10972 return this.$input
[ 0 ].value
.length
;
10976 * Focus the input and select the entire text.
10979 * @return {OO.ui.Widget} The widget, for chaining
10981 OO
.ui
.TextInputWidget
.prototype.select = function () {
10982 return this.selectRange( 0, this.getInputLength() );
10986 * Focus the input and move the cursor to the start.
10989 * @return {OO.ui.Widget} The widget, for chaining
10991 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10992 return this.selectRange( 0 );
10996 * Focus the input and move the cursor to the end.
10999 * @return {OO.ui.Widget} The widget, for chaining
11001 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
11002 return this.selectRange( this.getInputLength() );
11006 * Insert new content into the input.
11008 * @param {string} content Content to be inserted
11010 * @return {OO.ui.Widget} The widget, for chaining
11012 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
11014 range
= this.getRange(),
11015 value
= this.getValue();
11017 start
= Math
.min( range
.from, range
.to
);
11018 end
= Math
.max( range
.from, range
.to
);
11020 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
11021 this.selectRange( start
+ content
.length
);
11026 * Insert new content either side of a selection.
11028 * @param {string} pre Content to be inserted before the selection
11029 * @param {string} post Content to be inserted after the selection
11031 * @return {OO.ui.Widget} The widget, for chaining
11033 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
11035 range
= this.getRange(),
11036 offset
= pre
.length
;
11038 start
= Math
.min( range
.from, range
.to
);
11039 end
= Math
.max( range
.from, range
.to
);
11041 this.selectRange( start
).insertContent( pre
);
11042 this.selectRange( offset
+ end
).insertContent( post
);
11044 this.selectRange( offset
+ start
, offset
+ end
);
11049 * Set the validation pattern.
11051 * The validation pattern is either a regular expression, a function, or the symbolic name of a
11052 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11053 * value must contain only numbers).
11055 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
11056 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11058 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
11059 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
11060 this.validate
= validate
;
11062 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
11067 * Sets the 'invalid' flag appropriately.
11069 * @param {boolean} [isValid] Optionally override validation result
11071 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
11073 setFlag = function ( valid
) {
11075 widget
.$input
.attr( 'aria-invalid', 'true' );
11077 widget
.$input
.removeAttr( 'aria-invalid' );
11079 widget
.setFlags( { invalid
: !valid
} );
11082 if ( isValid
!== undefined ) {
11083 setFlag( isValid
);
11085 this.getValidity().then( function () {
11094 * Get the validity of current value.
11096 * This method returns a promise that resolves if the value is valid and rejects if
11097 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
11099 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
11101 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
11104 function rejectOrResolve( valid
) {
11106 return $.Deferred().resolve().promise();
11108 return $.Deferred().reject().promise();
11112 // Check browser validity and reject if it is invalid
11114 this.$input
[ 0 ].checkValidity
!== undefined &&
11115 this.$input
[ 0 ].checkValidity() === false
11117 return rejectOrResolve( false );
11120 // Run our checks if the browser thinks the field is valid
11121 if ( this.validate
instanceof Function
) {
11122 result
= this.validate( this.getValue() );
11123 if ( result
&& typeof result
.promise
=== 'function' ) {
11124 return result
.promise().then( function ( valid
) {
11125 return rejectOrResolve( valid
);
11128 return rejectOrResolve( result
);
11131 return rejectOrResolve( this.getValue().match( this.validate
) );
11136 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11138 * @param {string} labelPosition Label position, 'before' or 'after'
11140 * @return {OO.ui.Widget} The widget, for chaining
11142 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
11143 this.labelPosition
= labelPosition
;
11144 if ( this.label
) {
11145 // If there is no label and we only change the position, #updatePosition is a no-op,
11146 // but it takes really a lot of work to do nothing.
11147 this.updatePosition();
11153 * Update the position of the inline label.
11155 * This method is called by #setLabelPosition, and can also be called on its own if
11156 * something causes the label to be mispositioned.
11159 * @return {OO.ui.Widget} The widget, for chaining
11161 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
11162 var after
= this.labelPosition
=== 'after';
11165 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
11166 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
11168 this.valCache
= null;
11169 this.scrollWidth
= null;
11170 this.positionLabel();
11176 * Position the label by setting the correct padding on the input.
11180 * @return {OO.ui.Widget} The widget, for chaining
11182 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11183 var after
, rtl
, property
, newCss
;
11185 if ( this.isWaitingToBeAttached
) {
11186 // #onElementAttach will be called soon, which calls this method
11191 'padding-right': '',
11195 if ( this.label
) {
11196 this.$element
.append( this.$label
);
11198 this.$label
.detach();
11199 // Clear old values if present
11200 this.$input
.css( newCss
);
11204 after
= this.labelPosition
=== 'after';
11205 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11206 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11208 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11209 // We have to clear the padding on the other side, in case the element direction changed
11210 this.$input
.css( newCss
);
11216 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11217 * {@link OO.ui.mixin.IconElement search icon} by default.
11218 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11220 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11223 * @extends OO.ui.TextInputWidget
11226 * @param {Object} [config] Configuration options
11228 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11229 config
= $.extend( {
11233 // Parent constructor
11234 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11237 this.connect( this, {
11240 this.$indicator
.on( 'click', this.onIndicatorClick
.bind( this ) );
11243 this.updateSearchIndicator();
11244 this.connect( this, {
11245 disable
: 'onDisable'
11251 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11259 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11264 * Handle click events on the indicator
11266 * @param {jQuery.Event} e Click event
11267 * @return {boolean}
11269 OO
.ui
.SearchInputWidget
.prototype.onIndicatorClick = function ( e
) {
11270 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11271 // Clear the text field
11272 this.setValue( '' );
11279 * Update the 'clear' indicator displayed on type: 'search' text
11280 * fields, hiding it when the field is already empty or when it's not
11283 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11284 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11285 this.setIndicator( null );
11287 this.setIndicator( 'clear' );
11292 * Handle change events.
11296 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11297 this.updateSearchIndicator();
11301 * Handle disable events.
11303 * @param {boolean} disabled Element is disabled
11306 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11307 this.updateSearchIndicator();
11313 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11314 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11315 this.updateSearchIndicator();
11320 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11321 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11322 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11323 * {@link OO.ui.mixin.IndicatorElement indicators}.
11324 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11326 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11329 * // A MultilineTextInputWidget.
11330 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11331 * value: 'Text input on multiple lines'
11333 * $( document.body ).append( multilineTextInput.$element );
11335 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11338 * @extends OO.ui.TextInputWidget
11341 * @param {Object} [config] Configuration options
11342 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11343 * specifies minimum number of rows to display.
11344 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11345 * Use the #maxRows config to specify a maximum number of displayed rows.
11346 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11347 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11349 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11350 config
= $.extend( {
11353 // Parent constructor
11354 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11357 this.autosize
= !!config
.autosize
;
11358 this.styleHeight
= null;
11359 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11360 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11362 // Clone for resizing
11363 if ( this.autosize
) {
11364 this.$clone
= this.$input
11366 .removeAttr( 'id' )
11367 .removeAttr( 'name' )
11368 .insertAfter( this.$input
)
11369 .attr( 'aria-hidden', 'true' )
11370 .addClass( 'oo-ui-element-hidden' );
11374 this.connect( this, {
11379 if ( config
.rows
) {
11380 this.$input
.attr( 'rows', config
.rows
);
11382 if ( this.autosize
) {
11383 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11384 this.isWaitingToBeAttached
= true;
11385 this.installParentChangeDetector();
11391 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11393 /* Static Methods */
11398 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11399 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11400 state
.scrollTop
= config
.$input
.scrollTop();
11409 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11410 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11415 * Handle change events.
11419 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11426 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11427 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11434 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11436 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11438 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11439 // Some platforms emit keycode 10 for Control+Enter keypress in a textarea
11442 this.emit( 'enter', e
);
11447 * Automatically adjust the size of the text input.
11449 * This only affects multiline inputs that are {@link #autosize autosized}.
11452 * @return {OO.ui.Widget} The widget, for chaining
11455 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11456 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11457 idealHeight
, newHeight
, scrollWidth
, property
;
11459 if ( this.$input
.val() !== this.valCache
) {
11460 if ( this.autosize
) {
11462 .val( this.$input
.val() )
11463 .attr( 'rows', this.minRows
)
11464 // Set inline height property to 0 to measure scroll height
11465 .css( 'height', 0 );
11467 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11469 this.valCache
= this.$input
.val();
11471 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11473 // Remove inline height property to measure natural heights
11474 this.$clone
.css( 'height', '' );
11475 innerHeight
= this.$clone
.innerHeight();
11476 outerHeight
= this.$clone
.outerHeight();
11478 // Measure max rows height
11480 .attr( 'rows', this.maxRows
)
11481 .css( 'height', 'auto' )
11483 maxInnerHeight
= this.$clone
.innerHeight();
11485 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11486 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11487 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11488 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11490 this.$clone
.addClass( 'oo-ui-element-hidden' );
11492 // Only apply inline height when expansion beyond natural height is needed
11493 // Use the difference between the inner and outer height as a buffer
11494 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11495 if ( newHeight
!== this.styleHeight
) {
11496 this.$input
.css( 'height', newHeight
);
11497 this.styleHeight
= newHeight
;
11498 this.emit( 'resize' );
11501 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11502 if ( scrollWidth
!== this.scrollWidth
) {
11503 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11505 this.$label
.css( { right
: '', left
: '' } );
11506 this.$indicator
.css( { right
: '', left
: '' } );
11508 if ( scrollWidth
) {
11509 this.$indicator
.css( property
, scrollWidth
);
11510 if ( this.labelPosition
=== 'after' ) {
11511 this.$label
.css( property
, scrollWidth
);
11515 this.scrollWidth
= scrollWidth
;
11516 this.positionLabel();
11526 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11527 return $( '<textarea>' );
11531 * Check if the input automatically adjusts its size.
11533 * @return {boolean}
11535 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11536 return !!this.autosize
;
11542 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11543 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11544 if ( state
.scrollTop
!== undefined ) {
11545 this.$input
.scrollTop( state
.scrollTop
);
11550 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11551 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11552 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11554 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11555 * option, that option will appear to be selected.
11556 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11559 * After the user chooses an option, its `data` will be used as a new value for the widget.
11560 * A `label` also can be specified for each option: if given, it will be shown instead of the
11561 * `data` in the dropdown menu.
11563 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11565 * For more information about menus and options, please see the
11566 * [OOUI documentation on MediaWiki][1].
11569 * // A ComboBoxInputWidget.
11570 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11571 * value: 'Option 1',
11573 * { data: 'Option 1' },
11574 * { data: 'Option 2' },
11575 * { data: 'Option 3' }
11578 * $( document.body ).append( comboBox.$element );
11581 * // Example: A ComboBoxInputWidget with additional option labels.
11582 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11583 * value: 'Option 1',
11586 * data: 'Option 1',
11587 * label: 'Option One'
11590 * data: 'Option 2',
11591 * label: 'Option Two'
11594 * data: 'Option 3',
11595 * label: 'Option Three'
11599 * $( document.body ).append( comboBox.$element );
11601 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11604 * @extends OO.ui.TextInputWidget
11607 * @param {Object} [config] Configuration options
11608 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11609 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu
11611 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
11612 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
11613 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
11614 * uses relative positioning.
11615 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11617 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11618 // Configuration initialization
11619 config
= $.extend( {
11620 autocomplete
: false
11623 // ComboBoxInputWidget shouldn't support `multiline`
11624 config
.multiline
= false;
11626 // See InputWidget#reusePreInfuseDOM about `config.$input`
11627 if ( config
.$input
) {
11628 config
.$input
.removeAttr( 'list' );
11631 // Parent constructor
11632 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11635 this.$overlay
= ( config
.$overlay
=== true ?
11636 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11637 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11638 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11639 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11641 invisibleLabel
: true,
11642 disabled
: this.disabled
11644 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11648 $floatableContainer
: this.$element
,
11649 disabled
: this.isDisabled()
11655 this.connect( this, {
11656 change
: 'onInputChange',
11657 enter
: 'onInputEnter'
11659 this.dropdownButton
.connect( this, {
11660 click
: 'onDropdownButtonClick'
11662 this.menu
.connect( this, {
11663 choose
: 'onMenuChoose',
11664 add
: 'onMenuItemsChange',
11665 remove
: 'onMenuItemsChange',
11666 toggle
: 'onMenuToggle'
11670 this.$input
.attr( {
11672 'aria-owns': this.menu
.getElementId(),
11673 'aria-autocomplete': 'list'
11675 this.dropdownButton
.$button
.attr( {
11676 'aria-controls': this.menu
.getElementId()
11678 // Do not override options set via config.menu.items
11679 if ( config
.options
!== undefined ) {
11680 this.setOptions( config
.options
);
11682 this.$field
= $( '<div>' )
11683 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11684 .append( this.$input
, this.dropdownButton
.$element
);
11686 .addClass( 'oo-ui-comboBoxInputWidget' )
11687 .append( this.$field
);
11688 this.$overlay
.append( this.menu
.$element
);
11689 this.onMenuItemsChange();
11694 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11699 * Get the combobox's menu.
11701 * @return {OO.ui.MenuSelectWidget} Menu widget
11703 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11708 * Get the combobox's text input widget.
11710 * @return {OO.ui.TextInputWidget} Text input widget
11712 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11717 * Handle input change events.
11720 * @param {string} value New value
11722 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11723 var match
= this.menu
.findItemFromData( value
);
11725 this.menu
.selectItem( match
);
11726 if ( this.menu
.findHighlightedItem() ) {
11727 this.menu
.highlightItem( match
);
11730 if ( !this.isDisabled() ) {
11731 this.menu
.toggle( true );
11736 * Handle input enter events.
11740 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11741 if ( !this.isDisabled() ) {
11742 this.menu
.toggle( false );
11747 * Handle button click events.
11751 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11752 this.menu
.toggle();
11757 * Handle menu choose events.
11760 * @param {OO.ui.OptionWidget} item Chosen item
11762 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11763 this.setValue( item
.getData() );
11767 * Handle menu item change events.
11771 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11772 var match
= this.menu
.findItemFromData( this.getValue() );
11773 this.menu
.selectItem( match
);
11774 if ( this.menu
.findHighlightedItem() ) {
11775 this.menu
.highlightItem( match
);
11777 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11781 * Handle menu toggle events.
11784 * @param {boolean} isVisible Open state of the menu
11786 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11787 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11791 * Update the disabled state of the controls
11795 * @return {OO.ui.ComboBoxInputWidget} The widget, for chaining
11797 OO
.ui
.ComboBoxInputWidget
.prototype.updateControlsDisabled = function () {
11798 var disabled
= this.isDisabled() || this.isReadOnly();
11799 if ( this.dropdownButton
) {
11800 this.dropdownButton
.setDisabled( disabled
);
11803 this.menu
.setDisabled( disabled
);
11811 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function () {
11813 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.apply( this, arguments
);
11814 this.updateControlsDisabled();
11821 OO
.ui
.ComboBoxInputWidget
.prototype.setReadOnly = function () {
11823 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
11824 this.updateControlsDisabled();
11829 * Set the options available for this input.
11831 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11833 * @return {OO.ui.Widget} The widget, for chaining
11835 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11838 .addItems( options
.map( function ( opt
) {
11839 return new OO
.ui
.MenuOptionWidget( {
11841 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11849 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11850 * which is a widget that is specified by reference before any optional configuration settings.
11852 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of
11855 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11856 * A left-alignment is used for forms with many fields.
11857 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11858 * A right-alignment is used for long but familiar forms which users tab through,
11859 * verifying the current field with a quick glance at the label.
11860 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11861 * that users fill out from top to bottom.
11862 * - **inline**: The label is placed after the field-widget and aligned to the left.
11863 * An inline-alignment is best used with checkboxes or radio buttons.
11865 * Help text can either be:
11867 * - accessed via a help icon that appears in the upper right corner of the rendered field layout,
11869 * - shown as a subtle explanation below the label.
11871 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`.
11872 * If it is long or not essential, leave `helpInline` to its default, `false`.
11874 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11876 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11879 * @extends OO.ui.Layout
11880 * @mixins OO.ui.mixin.LabelElement
11881 * @mixins OO.ui.mixin.TitledElement
11884 * @param {OO.ui.Widget} fieldWidget Field widget
11885 * @param {Object} [config] Configuration options
11886 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11888 * @cfg {Array} [errors] Error messages about the widget, which will be
11889 * displayed below the widget.
11890 * @cfg {Array} [warnings] Warning messages about the widget, which will be
11891 * displayed below the widget.
11892 * @cfg {Array} [successMessages] Success messages on user interactions with the widget,
11893 * which will be displayed below the widget.
11894 * The array may contain strings or OO.ui.HtmlSnippet instances.
11895 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11896 * below the widget.
11897 * The array may contain strings or OO.ui.HtmlSnippet instances.
11898 * These are more visible than `help` messages when `helpInline` is set, and so
11899 * might be good for transient messages.
11900 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11901 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11902 * corner of the rendered field; clicking it will display the text in a popup.
11903 * If `helpInline` is `true`, then a subtle description will be shown after the
11905 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11906 * or shown when the "help" icon is clicked.
11907 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11909 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11911 * @throws {Error} An error is thrown if no widget is specified
11913 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11914 // Allow passing positional parameters inside the config object
11915 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11916 config
= fieldWidget
;
11917 fieldWidget
= config
.fieldWidget
;
11920 // Make sure we have required constructor arguments
11921 if ( fieldWidget
=== undefined ) {
11922 throw new Error( 'Widget not found' );
11925 // Configuration initialization
11926 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11928 // Parent constructor
11929 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11931 // Mixin constructors
11932 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
11933 $label
: $( '<label>' )
11935 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( { $titled
: this.$label
}, config
) );
11938 this.fieldWidget
= fieldWidget
;
11940 this.warnings
= [];
11941 this.successMessages
= [];
11943 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11944 this.$messages
= $( '<ul>' );
11945 this.$header
= $( '<span>' );
11946 this.$body
= $( '<div>' );
11948 this.helpInline
= config
.helpInline
;
11951 this.fieldWidget
.connect( this, {
11952 disable
: 'onFieldDisable'
11956 this.$help
= config
.help
?
11957 this.createHelpElement( config
.help
, config
.$overlay
) :
11959 if ( this.fieldWidget
.getInputId() ) {
11960 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11961 if ( this.helpInline
) {
11962 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
11965 this.$label
.on( 'click', function () {
11966 this.fieldWidget
.simulateLabelClick();
11968 if ( this.helpInline
) {
11969 this.$help
.on( 'click', function () {
11970 this.fieldWidget
.simulateLabelClick();
11975 .addClass( 'oo-ui-fieldLayout' )
11976 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11977 .append( this.$body
);
11978 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11979 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11980 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11982 .addClass( 'oo-ui-fieldLayout-field' )
11983 .append( this.fieldWidget
.$element
);
11985 this.setErrors( config
.errors
|| [] );
11986 this.setWarnings( config
.warnings
|| [] );
11987 this.setSuccess( config
.successMessages
|| [] );
11988 this.setNotices( config
.notices
|| [] );
11989 this.setAlignment( config
.align
);
11990 // Call this again to take into account the widget's accessKey
11991 this.updateTitle();
11996 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11997 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11998 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
12003 * Handle field disable events.
12006 * @param {boolean} value Field is disabled
12008 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
12009 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
12013 * Get the widget contained by the field.
12015 * @return {OO.ui.Widget} Field widget
12017 OO
.ui
.FieldLayout
.prototype.getField = function () {
12018 return this.fieldWidget
;
12022 * Return `true` if the given field widget can be used with `'inline'` alignment (see
12023 * #setAlignment). Return `false` if it can't or if this can't be determined.
12025 * @return {boolean}
12027 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
12028 // This is very simplistic, but should be good enough.
12029 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
12034 * @param {string} kind 'error' or 'notice'
12035 * @param {string|OO.ui.HtmlSnippet} text
12038 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
12039 var $listItem
, $icon
, message
;
12040 $listItem
= $( '<li>' );
12041 if ( kind
=== 'error' ) {
12042 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'error' ] } ).$element
;
12043 $listItem
.attr( 'role', 'alert' );
12044 } else if ( kind
=== 'warning' ) {
12045 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
12046 $listItem
.attr( 'role', 'alert' );
12047 } else if ( kind
=== 'success' ) {
12048 $icon
= new OO
.ui
.IconWidget( { icon
: 'check', flags
: [ 'success' ] } ).$element
;
12049 } else if ( kind
=== 'notice' ) {
12050 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
12054 message
= new OO
.ui
.LabelWidget( { label
: text
} );
12056 .append( $icon
, message
.$element
)
12057 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
12062 * Set the field alignment mode.
12065 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
12067 * @return {OO.ui.BookletLayout} The layout, for chaining
12069 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
12070 if ( value
!== this.align
) {
12071 // Default to 'left'
12072 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
12076 if ( value
=== 'inline' && !this.isFieldInline() ) {
12079 // Reorder elements
12081 if ( this.helpInline
) {
12082 if ( value
=== 'top' ) {
12083 this.$header
.append( this.$label
);
12084 this.$body
.append( this.$header
, this.$field
, this.$help
);
12085 } else if ( value
=== 'inline' ) {
12086 this.$header
.append( this.$label
, this.$help
);
12087 this.$body
.append( this.$field
, this.$header
);
12089 this.$header
.append( this.$label
, this.$help
);
12090 this.$body
.append( this.$header
, this.$field
);
12093 if ( value
=== 'top' ) {
12094 this.$header
.append( this.$help
, this.$label
);
12095 this.$body
.append( this.$header
, this.$field
);
12096 } else if ( value
=== 'inline' ) {
12097 this.$header
.append( this.$help
, this.$label
);
12098 this.$body
.append( this.$field
, this.$header
);
12100 this.$header
.append( this.$label
);
12101 this.$body
.append( this.$header
, this.$help
, this.$field
);
12104 // Set classes. The following classes can be used here:
12105 // * oo-ui-fieldLayout-align-left
12106 // * oo-ui-fieldLayout-align-right
12107 // * oo-ui-fieldLayout-align-top
12108 // * oo-ui-fieldLayout-align-inline
12109 if ( this.align
) {
12110 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
12112 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
12113 this.align
= value
;
12120 * Set the list of error messages.
12122 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
12123 * The array may contain strings or OO.ui.HtmlSnippet instances.
12125 * @return {OO.ui.BookletLayout} The layout, for chaining
12127 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
12128 this.errors
= errors
.slice();
12129 this.updateMessages();
12134 * Set the list of warning messages.
12136 * @param {Array} warnings Warning messages about the widget, which will be displayed below
12138 * The array may contain strings or OO.ui.HtmlSnippet instances.
12140 * @return {OO.ui.BookletLayout} The layout, for chaining
12142 OO
.ui
.FieldLayout
.prototype.setWarnings = function ( warnings
) {
12143 this.warnings
= warnings
.slice();
12144 this.updateMessages();
12149 * Set the list of success messages.
12151 * @param {Array} successMessages Success messages about the widget, which will be displayed below
12153 * The array may contain strings or OO.ui.HtmlSnippet instances.
12155 * @return {OO.ui.BookletLayout} The layout, for chaining
12157 OO
.ui
.FieldLayout
.prototype.setSuccess = function ( successMessages
) {
12158 this.successMessages
= successMessages
.slice();
12159 this.updateMessages();
12164 * Set the list of notice messages.
12166 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
12167 * The array may contain strings or OO.ui.HtmlSnippet instances.
12169 * @return {OO.ui.BookletLayout} The layout, for chaining
12171 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
12172 this.notices
= notices
.slice();
12173 this.updateMessages();
12178 * Update the rendering of error, warning, success and notice messages.
12182 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
12184 this.$messages
.empty();
12187 this.errors
.length
||
12188 this.warnings
.length
||
12189 this.successMessages
.length
||
12190 this.notices
.length
12192 this.$body
.after( this.$messages
);
12194 this.$messages
.remove();
12198 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
12199 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
12201 for ( i
= 0; i
< this.warnings
.length
; i
++ ) {
12202 this.$messages
.append( this.makeMessage( 'warning', this.warnings
[ i
] ) );
12204 for ( i
= 0; i
< this.successMessages
.length
; i
++ ) {
12205 this.$messages
.append( this.makeMessage( 'success', this.successMessages
[ i
] ) );
12207 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
12208 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
12213 * Include information about the widget's accessKey in our title. TitledElement calls this method.
12214 * (This is a bit of a hack.)
12217 * @param {string} title Tooltip label for 'title' attribute
12220 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
12221 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
12222 return this.fieldWidget
.formatTitleWithAccessKey( title
);
12228 * Creates and returns the help element. Also sets the `aria-describedby`
12229 * attribute on the main element of the `fieldWidget`.
12232 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
12233 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
12234 * @return {jQuery} The element that should become `this.$help`.
12236 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
12237 var helpId
, helpWidget
;
12239 if ( this.helpInline
) {
12240 helpWidget
= new OO
.ui
.LabelWidget( {
12242 classes
: [ 'oo-ui-inline-help' ]
12245 helpId
= helpWidget
.getElementId();
12247 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12248 $overlay
: $overlay
,
12252 classes
: [ 'oo-ui-fieldLayout-help' ],
12255 label
: OO
.ui
.msg( 'ooui-field-help' ),
12256 invisibleLabel
: true
12258 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
12259 helpWidget
.getPopup().$body
.html( help
.toString() );
12261 helpWidget
.getPopup().$body
.text( help
);
12264 helpId
= helpWidget
.getPopup().getBodyId();
12267 // Set the 'aria-describedby' attribute on the fieldWidget
12268 // Preference given to an input or a button
12270 this.fieldWidget
.$input
||
12271 this.fieldWidget
.$button
||
12272 this.fieldWidget
.$element
12273 ).attr( 'aria-describedby', helpId
);
12275 return helpWidget
.$element
;
12279 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget,
12280 * a button, and an optional label and/or help text. The field-widget (e.g., a
12281 * {@link OO.ui.TextInputWidget TextInputWidget}), is required and is specified before any optional
12282 * configuration settings.
12284 * Labels can be aligned in one of four ways:
12286 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12287 * A left-alignment is used for forms with many fields.
12288 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12289 * A right-alignment is used for long but familiar forms which users tab through,
12290 * verifying the current field with a quick glance at the label.
12291 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12292 * that users fill out from top to bottom.
12293 * - **inline**: The label is placed after the field-widget and aligned to the left.
12294 * An inline-alignment is best used with checkboxes or radio buttons.
12296 * Help text is accessed via a help icon that appears in the upper right corner of the rendered
12297 * field layout when help text is specified.
12300 * // Example of an ActionFieldLayout
12301 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12302 * new OO.ui.TextInputWidget( {
12303 * placeholder: 'Field widget'
12305 * new OO.ui.ButtonWidget( {
12309 * label: 'An ActionFieldLayout. This label is aligned top',
12311 * help: 'This is help text'
12315 * $( document.body ).append( actionFieldLayout.$element );
12318 * @extends OO.ui.FieldLayout
12321 * @param {OO.ui.Widget} fieldWidget Field widget
12322 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12323 * @param {Object} config
12325 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12326 // Allow passing positional parameters inside the config object
12327 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12328 config
= fieldWidget
;
12329 fieldWidget
= config
.fieldWidget
;
12330 buttonWidget
= config
.buttonWidget
;
12333 // Parent constructor
12334 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12337 this.buttonWidget
= buttonWidget
;
12338 this.$button
= $( '<span>' );
12339 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12342 this.$element
.addClass( 'oo-ui-actionFieldLayout' );
12344 .addClass( 'oo-ui-actionFieldLayout-button' )
12345 .append( this.buttonWidget
.$element
);
12347 .addClass( 'oo-ui-actionFieldLayout-input' )
12348 .append( this.fieldWidget
.$element
);
12349 this.$field
.append( this.$input
, this.$button
);
12354 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12357 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12358 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12359 * configured with a label as well. For more information and examples,
12360 * please see the [OOUI documentation on MediaWiki][1].
12363 * // Example of a fieldset layout
12364 * var input1 = new OO.ui.TextInputWidget( {
12365 * placeholder: 'A text input field'
12368 * var input2 = new OO.ui.TextInputWidget( {
12369 * placeholder: 'A text input field'
12372 * var fieldset = new OO.ui.FieldsetLayout( {
12373 * label: 'Example of a fieldset layout'
12376 * fieldset.addItems( [
12377 * new OO.ui.FieldLayout( input1, {
12378 * label: 'Field One'
12380 * new OO.ui.FieldLayout( input2, {
12381 * label: 'Field Two'
12384 * $( document.body ).append( fieldset.$element );
12386 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12389 * @extends OO.ui.Layout
12390 * @mixins OO.ui.mixin.IconElement
12391 * @mixins OO.ui.mixin.LabelElement
12392 * @mixins OO.ui.mixin.GroupElement
12395 * @param {Object} [config] Configuration options
12396 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset.
12397 * See OO.ui.FieldLayout for more information about fields.
12398 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon
12399 * will appear in the upper-right corner of the rendered field; clicking it will display the text
12400 * in a popup. For important messages, you are advised to use `notices`, as they are always shown.
12401 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12402 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12404 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12405 // Configuration initialization
12406 config
= config
|| {};
12408 // Parent constructor
12409 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12411 // Mixin constructors
12412 OO
.ui
.mixin
.IconElement
.call( this, config
);
12413 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12414 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12417 this.$header
= $( '<legend>' );
12418 if ( config
.help
) {
12419 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
12420 $overlay
: config
.$overlay
,
12424 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12427 label
: OO
.ui
.msg( 'ooui-field-help' ),
12428 invisibleLabel
: true
12430 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12431 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
12433 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
12435 this.$help
= this.popupButtonWidget
.$element
;
12437 this.$help
= $( [] );
12442 .addClass( 'oo-ui-fieldsetLayout-header' )
12443 .append( this.$icon
, this.$label
, this.$help
);
12444 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12446 .addClass( 'oo-ui-fieldsetLayout' )
12447 .prepend( this.$header
, this.$group
);
12448 if ( Array
.isArray( config
.items
) ) {
12449 this.addItems( config
.items
);
12455 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12456 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12457 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12458 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12460 /* Static Properties */
12466 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12469 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use
12470 * browser-based form submission for the fields instead of handling them in JavaScript. Form layouts
12471 * can be configured with an HTML form action, an encoding type, and a method using the #action,
12472 * #enctype, and #method configs, respectively.
12473 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12475 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12476 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12477 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12478 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12479 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12480 * often have simplified APIs to match the capabilities of HTML forms.
12481 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12483 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12484 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12487 * // Example of a form layout that wraps a fieldset layout.
12488 * var input1 = new OO.ui.TextInputWidget( {
12489 * placeholder: 'Username'
12491 * input2 = new OO.ui.TextInputWidget( {
12492 * placeholder: 'Password',
12495 * submit = new OO.ui.ButtonInputWidget( {
12498 * fieldset = new OO.ui.FieldsetLayout( {
12499 * label: 'A form layout'
12502 * fieldset.addItems( [
12503 * new OO.ui.FieldLayout( input1, {
12504 * label: 'Username',
12507 * new OO.ui.FieldLayout( input2, {
12508 * label: 'Password',
12511 * new OO.ui.FieldLayout( submit )
12513 * var form = new OO.ui.FormLayout( {
12514 * items: [ fieldset ],
12515 * action: '/api/formhandler',
12518 * $( document.body ).append( form.$element );
12521 * @extends OO.ui.Layout
12522 * @mixins OO.ui.mixin.GroupElement
12525 * @param {Object} [config] Configuration options
12526 * @cfg {string} [method] HTML form `method` attribute
12527 * @cfg {string} [action] HTML form `action` attribute
12528 * @cfg {string} [enctype] HTML form `enctype` attribute
12529 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12531 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12534 // Configuration initialization
12535 config
= config
|| {};
12537 // Parent constructor
12538 OO
.ui
.FormLayout
.parent
.call( this, config
);
12540 // Mixin constructors
12541 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12544 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12546 // Make sure the action is safe
12547 action
= config
.action
;
12548 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12549 action
= './' + action
;
12554 .addClass( 'oo-ui-formLayout' )
12556 method
: config
.method
,
12558 enctype
: config
.enctype
12560 if ( Array
.isArray( config
.items
) ) {
12561 this.addItems( config
.items
);
12567 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12568 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12573 * A 'submit' event is emitted when the form is submitted.
12578 /* Static Properties */
12584 OO
.ui
.FormLayout
.static.tagName
= 'form';
12589 * Handle form submit events.
12592 * @param {jQuery.Event} e Submit event
12594 * @return {OO.ui.FormLayout} The layout, for chaining
12596 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12597 if ( this.emit( 'submit' ) ) {
12603 * PanelLayouts expand to cover the entire area of their parent. They can be configured with
12604 * scrolling, padding, and a frame, and are often used together with
12605 * {@link OO.ui.StackLayout StackLayouts}.
12608 * // Example of a panel layout
12609 * var panel = new OO.ui.PanelLayout( {
12613 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12615 * $( document.body ).append( panel.$element );
12618 * @extends OO.ui.Layout
12621 * @param {Object} [config] Configuration options
12622 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12623 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12624 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12625 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside
12628 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12629 // Configuration initialization
12630 config
= $.extend( {
12637 // Parent constructor
12638 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12641 this.$element
.addClass( 'oo-ui-panelLayout' );
12642 if ( config
.scrollable
) {
12643 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12645 if ( config
.padded
) {
12646 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12648 if ( config
.expanded
) {
12649 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12651 if ( config
.framed
) {
12652 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12658 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12660 /* Static Methods */
12665 OO
.ui
.PanelLayout
.static.reusePreInfuseDOM = function ( node
, config
) {
12666 config
= OO
.ui
.PanelLayout
.parent
.static.reusePreInfuseDOM( node
, config
);
12667 if ( config
.preserveContent
!== false ) {
12668 config
.$content
= $( node
).contents();
12676 * Focus the panel layout
12678 * The default implementation just focuses the first focusable element in the panel
12680 OO
.ui
.PanelLayout
.prototype.focus = function () {
12681 OO
.ui
.findFocusable( this.$element
).focus();
12685 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12686 * items), with small margins between them. Convenient when you need to put a number of block-level
12687 * widgets on a single line next to each other.
12689 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12692 * // HorizontalLayout with a text input and a label.
12693 * var layout = new OO.ui.HorizontalLayout( {
12695 * new OO.ui.LabelWidget( { label: 'Label' } ),
12696 * new OO.ui.TextInputWidget( { value: 'Text' } )
12699 * $( document.body ).append( layout.$element );
12702 * @extends OO.ui.Layout
12703 * @mixins OO.ui.mixin.GroupElement
12706 * @param {Object} [config] Configuration options
12707 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12709 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12710 // Configuration initialization
12711 config
= config
|| {};
12713 // Parent constructor
12714 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12716 // Mixin constructors
12717 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12720 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12721 if ( Array
.isArray( config
.items
) ) {
12722 this.addItems( config
.items
);
12728 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12729 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12732 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12733 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12734 * (to adjust the value in increments) to allow the user to enter a number.
12737 * // A NumberInputWidget.
12738 * var numberInput = new OO.ui.NumberInputWidget( {
12739 * label: 'NumberInputWidget',
12740 * input: { value: 5 },
12744 * $( document.body ).append( numberInput.$element );
12747 * @extends OO.ui.TextInputWidget
12750 * @param {Object} [config] Configuration options
12751 * @cfg {Object} [minusButton] Configuration options to pass to the
12752 * {@link OO.ui.ButtonWidget decrementing button widget}.
12753 * @cfg {Object} [plusButton] Configuration options to pass to the
12754 * {@link OO.ui.ButtonWidget incrementing button widget}.
12755 * @cfg {number} [min=-Infinity] Minimum allowed value
12756 * @cfg {number} [max=Infinity] Maximum allowed value
12757 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12758 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or Up/Down arrow keys.
12759 * Defaults to `step` if specified, otherwise `1`.
12760 * @cfg {number} [pageStep=10*buttonStep] Delta when using the Page-up/Page-down keys.
12761 * Defaults to 10 times `buttonStep`.
12762 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12764 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12765 var $field
= $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' );
12767 // Configuration initialization
12768 config
= $.extend( {
12774 // For backward compatibility
12775 $.extend( config
, config
.input
);
12778 // Parent constructor
12779 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12783 if ( config
.showButtons
) {
12784 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12786 disabled
: this.isDisabled(),
12788 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12793 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12794 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12796 disabled
: this.isDisabled(),
12798 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12803 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12808 keydown
: this.onKeyDown
.bind( this ),
12809 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12811 if ( config
.showButtons
) {
12812 this.plusButton
.connect( this, {
12813 click
: [ 'onButtonClick', +1 ]
12815 this.minusButton
.connect( this, {
12816 click
: [ 'onButtonClick', -1 ]
12821 $field
.append( this.$input
);
12822 if ( config
.showButtons
) {
12824 .prepend( this.minusButton
.$element
)
12825 .append( this.plusButton
.$element
);
12829 if ( config
.allowInteger
|| config
.isInteger
) {
12830 // Backward compatibility
12833 this.setRange( config
.min
, config
.max
);
12834 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
12835 // Set the validation method after we set step and range
12836 // so that it doesn't immediately call setValidityFlag
12837 this.setValidation( this.validateNumber
.bind( this ) );
12840 .addClass( 'oo-ui-numberInputWidget' )
12841 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12847 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12851 // Backward compatibility
12852 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12853 this.setStep( flag
? 1 : null );
12855 // Backward compatibility
12856 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12858 // Backward compatibility
12859 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12860 return this.step
=== 1;
12862 // Backward compatibility
12863 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12866 * Set the range of allowed values
12868 * @param {number} min Minimum allowed value
12869 * @param {number} max Maximum allowed value
12871 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12873 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12877 this.$input
.attr( 'min', this.min
);
12878 this.$input
.attr( 'max', this.max
);
12879 this.setValidityFlag();
12883 * Get the current range
12885 * @return {number[]} Minimum and maximum values
12887 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12888 return [ this.min
, this.max
];
12892 * Set the stepping deltas
12894 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12895 * Defaults to `step` if specified, otherwise `1`.
12896 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12897 * Defaults to 10 times `buttonStep`.
12898 * @param {number|null} [step] If specified, the field only accepts values that are multiples
12901 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
12902 if ( buttonStep
=== undefined ) {
12903 buttonStep
= step
|| 1;
12905 if ( pageStep
=== undefined ) {
12906 pageStep
= 10 * buttonStep
;
12908 if ( step
!== null && step
<= 0 ) {
12909 throw new Error( 'Step value, if given, must be positive' );
12911 if ( buttonStep
<= 0 ) {
12912 throw new Error( 'Button step value must be positive' );
12914 if ( pageStep
<= 0 ) {
12915 throw new Error( 'Page step value must be positive' );
12918 this.buttonStep
= buttonStep
;
12919 this.pageStep
= pageStep
;
12920 this.$input
.attr( 'step', this.step
|| 'any' );
12921 this.setValidityFlag();
12927 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12928 if ( value
=== '' ) {
12929 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12930 // so here we make sure an 'empty' value is actually displayed as such.
12931 this.$input
.val( '' );
12933 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12937 * Get the current stepping values
12939 * @return {number[]} Button step, page step, and validity step
12941 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12942 return [ this.buttonStep
, this.pageStep
, this.step
];
12946 * Get the current value of the widget as a number
12948 * @return {number} May be NaN, or an invalid number
12950 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12951 return +this.getValue();
12955 * Adjust the value of the widget
12957 * @param {number} delta Adjustment amount
12959 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12960 var n
, v
= this.getNumericValue();
12963 if ( isNaN( delta
) || !isFinite( delta
) ) {
12964 throw new Error( 'Delta must be a finite number' );
12967 if ( isNaN( v
) ) {
12971 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12973 n
= Math
.round( n
/ this.step
) * this.step
;
12978 this.setValue( n
);
12985 * @param {string} value Field value
12986 * @return {boolean}
12988 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12990 if ( value
=== '' ) {
12991 return !this.isRequired();
12994 if ( isNaN( n
) || !isFinite( n
) ) {
12998 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
13002 if ( n
< this.min
|| n
> this.max
) {
13010 * Handle mouse click events.
13013 * @param {number} dir +1 or -1
13015 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
13016 this.adjustValue( dir
* this.buttonStep
);
13020 * Handle mouse wheel events.
13023 * @param {jQuery.Event} event
13024 * @return {undefined/boolean} False to prevent default if event is handled
13026 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
13029 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
13030 // Standard 'wheel' event
13031 if ( event
.originalEvent
.deltaMode
!== undefined ) {
13032 this.sawWheelEvent
= true;
13034 if ( event
.originalEvent
.deltaY
) {
13035 delta
= -event
.originalEvent
.deltaY
;
13036 } else if ( event
.originalEvent
.deltaX
) {
13037 delta
= event
.originalEvent
.deltaX
;
13040 // Non-standard events
13041 if ( !this.sawWheelEvent
) {
13042 if ( event
.originalEvent
.wheelDeltaX
) {
13043 delta
= -event
.originalEvent
.wheelDeltaX
;
13044 } else if ( event
.originalEvent
.wheelDeltaY
) {
13045 delta
= event
.originalEvent
.wheelDeltaY
;
13046 } else if ( event
.originalEvent
.wheelDelta
) {
13047 delta
= event
.originalEvent
.wheelDelta
;
13048 } else if ( event
.originalEvent
.detail
) {
13049 delta
= -event
.originalEvent
.detail
;
13054 delta
= delta
< 0 ? -1 : 1;
13055 this.adjustValue( delta
* this.buttonStep
);
13063 * Handle key down events.
13066 * @param {jQuery.Event} e Key down event
13067 * @return {undefined/boolean} False to prevent default if event is handled
13069 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
13070 if ( !this.isDisabled() ) {
13071 switch ( e
.which
) {
13072 case OO
.ui
.Keys
.UP
:
13073 this.adjustValue( this.buttonStep
);
13075 case OO
.ui
.Keys
.DOWN
:
13076 this.adjustValue( -this.buttonStep
);
13078 case OO
.ui
.Keys
.PAGEUP
:
13079 this.adjustValue( this.pageStep
);
13081 case OO
.ui
.Keys
.PAGEDOWN
:
13082 this.adjustValue( -this.pageStep
);
13091 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
13093 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13095 if ( this.minusButton
) {
13096 this.minusButton
.setDisabled( this.isDisabled() );
13098 if ( this.plusButton
) {
13099 this.plusButton
.setDisabled( this.isDisabled() );
13106 * SelectFileInputWidgets allow for selecting files, using <input type="file">. These
13107 * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
13108 * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
13109 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
13111 * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename.
13114 * // A file select input widget.
13115 * var selectFile = new OO.ui.SelectFileInputWidget();
13116 * $( document.body ).append( selectFile.$element );
13118 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
13121 * @extends OO.ui.InputWidget
13124 * @param {Object} [config] Configuration options
13125 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
13126 * @cfg {string} [placeholder] Text to display when no file is selected.
13127 * @cfg {Object} [button] Config to pass to select file button.
13128 * @cfg {string} [icon] Icon to show next to file info
13130 OO
.ui
.SelectFileInputWidget
= function OoUiSelectFileInputWidget( config
) {
13131 config
= config
|| {};
13133 // Construct buttons before parent method is called (calling setDisabled)
13134 this.selectButton
= new OO
.ui
.ButtonWidget( $.extend( {
13135 $element
: $( '<label>' ),
13136 classes
: [ 'oo-ui-selectFileInputWidget-selectButton' ],
13137 label
: OO
.ui
.msg( 'ooui-selectfile-button-select' )
13138 }, config
.button
) );
13140 // Configuration initialization
13141 config
= $.extend( {
13143 placeholder
: OO
.ui
.msg( 'ooui-selectfile-placeholder' ),
13144 $tabIndexed
: this.selectButton
.$tabIndexed
13147 this.info
= new OO
.ui
.SearchInputWidget( {
13148 classes
: [ 'oo-ui-selectFileInputWidget-info' ],
13149 placeholder
: config
.placeholder
,
13150 // Pass an empty collection so that .focus() always does nothing
13151 $tabIndexed
: $( [] )
13152 } ).setIcon( config
.icon
);
13153 // Set tabindex manually on $input as $tabIndexed has been overridden
13154 this.info
.$input
.attr( 'tabindex', -1 );
13156 // Parent constructor
13157 OO
.ui
.SelectFileInputWidget
.parent
.call( this, config
);
13160 this.currentFile
= null;
13161 if ( Array
.isArray( config
.accept
) ) {
13162 this.accept
= config
.accept
;
13164 this.accept
= null;
13166 this.onFileSelectedHandler
= this.onFileSelected
.bind( this );
13169 this.info
.connect( this, { change
: 'onInfoChange' } );
13170 this.selectButton
.$button
.on( {
13171 keypress
: this.onKeyPress
.bind( this )
13173 this.connect( this, { change
: 'updateUI' } );
13178 this.fieldLayout
= new OO
.ui
.ActionFieldLayout( this.info
, this.selectButton
, { align
: 'top' } );
13181 .addClass( 'oo-ui-selectFileInputWidget' )
13182 .append( this.fieldLayout
.$element
);
13189 OO
.inheritClass( OO
.ui
.SelectFileInputWidget
, OO
.ui
.InputWidget
);
13194 * Get the filename of the currently selected file.
13196 * @return {string} Filename
13198 OO
.ui
.SelectFileInputWidget
.prototype.getFilename = function () {
13199 if ( this.currentFile
) {
13200 return this.currentFile
.name
;
13202 // Try to strip leading fakepath.
13203 return this.getValue().split( '\\' ).pop();
13210 OO
.ui
.SelectFileInputWidget
.prototype.setValue = function ( value
) {
13211 if ( value
=== undefined ) {
13212 // Called during init, don't replace value if just infusing.
13216 // We need to update this.value, but without trying to modify
13217 // the DOM value, which would throw an exception.
13218 if ( this.value
!== value
) {
13219 this.value
= value
;
13220 this.emit( 'change', this.value
);
13223 this.currentFile
= null;
13225 OO
.ui
.SelectFileInputWidget
.super.prototype.setValue
.call( this, '' );
13230 * Handle file selection from the input.
13233 * @param {jQuery.Event} e
13235 OO
.ui
.SelectFileInputWidget
.prototype.onFileSelected = function ( e
) {
13236 var file
= OO
.getProp( e
.target
, 'files', 0 ) || null;
13238 if ( file
&& !this.isAllowedType( file
.type
) ) {
13242 this.currentFile
= file
;
13246 * Update the user interface when a file is selected or unselected.
13250 OO
.ui
.SelectFileInputWidget
.prototype.updateUI = function () {
13251 this.info
.setValue( this.getFilename() );
13255 * Setup the input element.
13259 OO
.ui
.SelectFileInputWidget
.prototype.setupInput = function () {
13264 // this.selectButton is tabindexed
13266 // Infused input may have previously by
13267 // TabIndexed, so remove aria-disabled attr.
13268 'aria-disabled': null
13270 .on( 'change', this.onFileSelectedHandler
)
13272 // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
13273 // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
13274 // Since this messes with our custom styling (the file input has large dimensions and this
13275 // causes the label to scroll out of view), scroll the button back to top. (T192131)
13276 .on( 'focus', function () {
13277 widget
.$input
.parent().prop( 'scrollTop', 0 );
13280 if ( this.accept
) {
13281 this.$input
.attr( 'accept', this.accept
.join( ', ' ) );
13283 this.selectButton
.$button
.append( this.$input
);
13287 * Determine if we should accept this file.
13290 * @param {string} mimeType File MIME type
13291 * @return {boolean}
13293 OO
.ui
.SelectFileInputWidget
.prototype.isAllowedType = function ( mimeType
) {
13296 if ( !this.accept
|| !mimeType
) {
13300 for ( i
= 0; i
< this.accept
.length
; i
++ ) {
13301 mimeTest
= this.accept
[ i
];
13302 if ( mimeTest
=== mimeType
) {
13304 } else if ( mimeTest
.substr( -2 ) === '/*' ) {
13305 mimeTest
= mimeTest
.substr( 0, mimeTest
.length
- 1 );
13306 if ( mimeType
.substr( 0, mimeTest
.length
) === mimeTest
) {
13316 * Handle info input change events
13318 * The info widget can only be changed by the user
13319 * with the clear button.
13322 * @param {string} value
13324 OO
.ui
.SelectFileInputWidget
.prototype.onInfoChange = function ( value
) {
13325 if ( value
=== '' ) {
13326 this.setValue( null );
13331 * Handle key press events.
13334 * @param {jQuery.Event} e Key press event
13335 * @return {undefined/boolean} False to prevent default if event is handled
13337 OO
.ui
.SelectFileInputWidget
.prototype.onKeyPress = function ( e
) {
13338 if ( !this.isDisabled() && this.$input
&&
13339 ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
13341 // Emit a click to open the file selector.
13342 this.$input
.trigger( 'click' );
13343 // Taking focus from the selectButton means keyUp isn't fired, so fire it manually.
13344 this.selectButton
.onDocumentKeyUp( e
);
13352 OO
.ui
.SelectFileInputWidget
.prototype.setDisabled = function ( disabled
) {
13354 OO
.ui
.SelectFileInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13356 this.selectButton
.setDisabled( disabled
);
13357 this.info
.setDisabled( disabled
);
13364 //# sourceMappingURL=oojs-ui-core.js.map.json