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-21T15:54:37Z
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
9471 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9472 // Configuration initialization
9473 config
= config
|| {};
9475 // Parent constructor
9476 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9479 this.checkIcon
= new OO
.ui
.IconWidget( {
9481 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9486 .addClass( 'oo-ui-checkboxInputWidget' )
9487 // Required for pretty styling in WikimediaUI theme
9488 .append( this.checkIcon
.$element
);
9489 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9494 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9496 /* Static Properties */
9502 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9504 /* Static Methods */
9509 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9510 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9511 state
.checked
= config
.$input
.prop( 'checked' );
9521 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9522 return $( '<input>' ).attr( 'type', 'checkbox' );
9528 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9530 if ( !this.isDisabled() ) {
9531 // Allow the stack to clear so the value will be updated
9532 setTimeout( function () {
9533 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9539 * Set selection state of this checkbox.
9541 * @param {boolean} state `true` for selected
9543 * @return {OO.ui.Widget} The widget, for chaining
9545 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9547 if ( this.selected
!== state
) {
9548 this.selected
= state
;
9549 this.$input
.prop( 'checked', this.selected
);
9550 this.emit( 'change', this.selected
);
9552 // The first time that the selection state is set (probably while constructing the widget),
9553 // remember it in defaultSelected. This property can be later used to check whether
9554 // the selection state of the input has been changed since it was created.
9555 if ( this.defaultSelected
=== undefined ) {
9556 this.defaultSelected
= this.selected
;
9557 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9563 * Check if this checkbox is selected.
9565 * @return {boolean} Checkbox is selected
9567 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9568 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9569 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9570 var selected
= this.$input
.prop( 'checked' );
9571 if ( this.selected
!== selected
) {
9572 this.setSelected( selected
);
9574 return this.selected
;
9580 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9581 if ( !this.isDisabled() ) {
9582 this.$handle
.trigger( 'click' );
9590 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9591 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9592 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9593 this.setSelected( state
.checked
);
9598 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9599 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the
9600 * value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9601 * more information about input widgets.
9603 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9604 * are no options. If no `value` configuration option is provided, the first option is selected.
9605 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9607 * This and OO.ui.RadioSelectInputWidget support similar configuration options.
9610 * // A DropdownInputWidget with three options.
9611 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9613 * { data: 'a', label: 'First' },
9614 * { data: 'b', label: 'Second', disabled: true },
9615 * { optgroup: 'Group label' },
9616 * { data: 'c', label: 'First sub-item)' }
9619 * $( document.body ).append( dropdownInput.$element );
9621 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9624 * @extends OO.ui.InputWidget
9627 * @param {Object} [config] Configuration options
9628 * @cfg {Object[]} [options=[]] Array of menu options in the format described above.
9629 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9630 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
9631 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
9632 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
9633 * uses relative positioning.
9634 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9636 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9637 // Configuration initialization
9638 config
= config
|| {};
9640 // Properties (must be done before parent constructor which calls #setDisabled)
9641 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9643 $overlay
: config
.$overlay
9647 // Set up the options before parent constructor, which uses them to validate config.value.
9648 // Use this instead of setOptions() because this.$input is not set up yet.
9649 this.setOptionsData( config
.options
|| [] );
9651 // Parent constructor
9652 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9655 this.dropdownWidget
.getMenu().connect( this, {
9656 select
: 'onMenuSelect'
9661 .addClass( 'oo-ui-dropdownInputWidget' )
9662 .append( this.dropdownWidget
.$element
);
9663 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9664 this.setTitledElement( this.dropdownWidget
.$handle
);
9669 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9677 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9678 return $( '<select>' );
9682 * Handles menu select events.
9685 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9687 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9688 this.setValue( item
? item
.getData() : '' );
9694 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9696 value
= this.cleanUpValue( value
);
9697 // Only allow setting values that are actually present in the dropdown
9698 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9699 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9700 this.dropdownWidget
.getMenu().selectItem( selected
);
9701 value
= selected
? selected
.getData() : '';
9702 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9703 if ( this.optionsDirty
) {
9704 // We reached this from the constructor or from #setOptions.
9705 // We have to update the <select> element.
9706 this.updateOptionsInterface();
9714 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9715 this.dropdownWidget
.setDisabled( state
);
9716 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9721 * Set the options available for this input.
9723 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9725 * @return {OO.ui.Widget} The widget, for chaining
9727 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9728 var value
= this.getValue();
9730 this.setOptionsData( options
);
9732 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9733 // In case the previous value is no longer an available option, select the first valid one.
9734 this.setValue( value
);
9740 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9742 * This method may be called before the parent constructor, so various properties may not be
9745 * @param {Object[]} options Array of menu options (see #constructor for details).
9748 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9749 var optionWidgets
, optIndex
, opt
, previousOptgroup
, optionWidget
, optValue
,
9752 this.optionsDirty
= true;
9754 // Go through all the supplied option configs and create either
9755 // MenuSectionOption or MenuOption widgets from each.
9757 for ( optIndex
= 0; optIndex
< options
.length
; optIndex
++ ) {
9758 opt
= options
[ optIndex
];
9760 if ( opt
.optgroup
!== undefined ) {
9761 // Create a <optgroup> menu item.
9762 optionWidget
= widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9763 previousOptgroup
= optionWidget
;
9766 // Create a normal <option> menu item.
9767 optValue
= widget
.cleanUpValue( opt
.data
);
9768 optionWidget
= widget
.createMenuOptionWidget(
9770 opt
.label
!== undefined ? opt
.label
: optValue
9774 // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
9776 opt
.disabled
!== undefined ||
9777 previousOptgroup
instanceof OO
.ui
.MenuSectionOptionWidget
&&
9778 previousOptgroup
.isDisabled()
9780 optionWidget
.setDisabled( true );
9783 optionWidgets
.push( optionWidget
);
9786 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9790 * Create a menu option widget.
9793 * @param {string} data Item data
9794 * @param {string} label Item label
9795 * @return {OO.ui.MenuOptionWidget} Option widget
9797 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9798 return new OO
.ui
.MenuOptionWidget( {
9805 * Create a menu section option widget.
9808 * @param {string} label Section item label
9809 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9811 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9812 return new OO
.ui
.MenuSectionOptionWidget( {
9818 * Update the user-visible interface to match the internal list of options and value.
9820 * This method must only be called after the parent constructor.
9824 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9826 $optionsContainer
= this.$input
,
9827 defaultValue
= this.defaultValue
,
9830 this.$input
.empty();
9832 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9835 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9836 $optionNode
= $( '<option>' )
9837 .attr( 'value', optionWidget
.getData() )
9838 .text( optionWidget
.getLabel() );
9840 // Remember original selection state. This property can be later used to check whether
9841 // the selection state of the input has been changed since it was created.
9842 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9844 $optionsContainer
.append( $optionNode
);
9846 $optionNode
= $( '<optgroup>' )
9847 .attr( 'label', optionWidget
.getLabel() );
9848 widget
.$input
.append( $optionNode
);
9849 $optionsContainer
= $optionNode
;
9852 // Disable the option or optgroup if required.
9853 if ( optionWidget
.isDisabled() ) {
9854 $optionNode
.prop( 'disabled', true );
9858 this.optionsDirty
= false;
9864 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9865 this.dropdownWidget
.focus();
9872 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9873 this.dropdownWidget
.blur();
9878 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9879 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9880 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9881 * please see the [OOUI documentation on MediaWiki][1].
9883 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9886 * // An example of selected, unselected, and disabled radio inputs
9887 * var radio1 = new OO.ui.RadioInputWidget( {
9891 * var radio2 = new OO.ui.RadioInputWidget( {
9894 * var radio3 = new OO.ui.RadioInputWidget( {
9898 * // Create a fieldset layout with fields for each radio button.
9899 * var fieldset = new OO.ui.FieldsetLayout( {
9900 * label: 'Radio inputs'
9902 * fieldset.addItems( [
9903 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9904 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9905 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9907 * $( document.body ).append( fieldset.$element );
9909 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9912 * @extends OO.ui.InputWidget
9915 * @param {Object} [config] Configuration options
9916 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button
9919 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9920 // Configuration initialization
9921 config
= config
|| {};
9923 // Parent constructor
9924 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9928 .addClass( 'oo-ui-radioInputWidget' )
9929 // Required for pretty styling in WikimediaUI theme
9930 .append( $( '<span>' ) );
9931 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9936 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9938 /* Static Properties */
9944 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9946 /* Static Methods */
9951 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9952 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9953 state
.checked
= config
.$input
.prop( 'checked' );
9963 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9964 return $( '<input>' ).attr( 'type', 'radio' );
9970 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9971 // RadioInputWidget doesn't track its state.
9975 * Set selection state of this radio button.
9977 * @param {boolean} state `true` for selected
9979 * @return {OO.ui.Widget} The widget, for chaining
9981 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9982 // RadioInputWidget doesn't track its state.
9983 this.$input
.prop( 'checked', state
);
9984 // The first time that the selection state is set (probably while constructing the widget),
9985 // remember it in defaultSelected. This property can be later used to check whether
9986 // the selection state of the input has been changed since it was created.
9987 if ( this.defaultSelected
=== undefined ) {
9988 this.defaultSelected
= state
;
9989 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9995 * Check if this radio button is selected.
9997 * @return {boolean} Radio is selected
9999 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
10000 return this.$input
.prop( 'checked' );
10006 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
10007 if ( !this.isDisabled() ) {
10008 this.$input
.trigger( 'click' );
10016 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10017 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10018 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
10019 this.setSelected( state
.checked
);
10024 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be
10025 * used within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with
10026 * the value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
10027 * more information about input widgets.
10029 * This and OO.ui.DropdownInputWidget support similar configuration options.
10032 * // A RadioSelectInputWidget with three options
10033 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
10035 * { data: 'a', label: 'First' },
10036 * { data: 'b', label: 'Second'},
10037 * { data: 'c', label: 'Third' }
10040 * $( document.body ).append( radioSelectInput.$element );
10042 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10045 * @extends OO.ui.InputWidget
10048 * @param {Object} [config] Configuration options
10049 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10051 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
10052 // Configuration initialization
10053 config
= config
|| {};
10055 // Properties (must be done before parent constructor which calls #setDisabled)
10056 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
10057 // Set up the options before parent constructor, which uses them to validate config.value.
10058 // Use this instead of setOptions() because this.$input is not set up yet
10059 this.setOptionsData( config
.options
|| [] );
10061 // Parent constructor
10062 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
10065 this.radioSelectWidget
.connect( this, {
10066 select
: 'onMenuSelect'
10071 .addClass( 'oo-ui-radioSelectInputWidget' )
10072 .append( this.radioSelectWidget
.$element
);
10073 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
10078 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
10080 /* Static Methods */
10085 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10086 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10087 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
10094 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10095 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10096 // Cannot reuse the `<input type=radio>` set
10097 delete config
.$input
;
10107 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
10108 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
10109 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
10110 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
10114 * Handles menu select events.
10117 * @param {OO.ui.RadioOptionWidget} item Selected menu item
10119 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
10120 this.setValue( item
.getData() );
10126 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
10128 value
= this.cleanUpValue( value
);
10129 // Only allow setting values that are actually present in the dropdown
10130 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
10131 this.radioSelectWidget
.findFirstSelectableItem();
10132 this.radioSelectWidget
.selectItem( selected
);
10133 value
= selected
? selected
.getData() : '';
10134 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10141 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10142 this.radioSelectWidget
.setDisabled( state
);
10143 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10148 * Set the options available for this input.
10150 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10152 * @return {OO.ui.Widget} The widget, for chaining
10154 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10155 var value
= this.getValue();
10157 this.setOptionsData( options
);
10159 // Re-set the value to update the visible interface (RadioSelectWidget).
10160 // In case the previous value is no longer an available option, select the first valid one.
10161 this.setValue( value
);
10167 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10169 * This method may be called before the parent constructor, so various properties may not be
10172 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10175 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10178 this.radioSelectWidget
10180 .addItems( options
.map( function ( opt
) {
10181 var optValue
= widget
.cleanUpValue( opt
.data
);
10182 return new OO
.ui
.RadioOptionWidget( {
10184 label
: opt
.label
!== undefined ? opt
.label
: optValue
10192 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10193 this.radioSelectWidget
.focus();
10200 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10201 this.radioSelectWidget
.blur();
10206 * CheckboxMultiselectInputWidget is a
10207 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10208 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10209 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10210 * more information about input widgets.
10213 * // A CheckboxMultiselectInputWidget with three options.
10214 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10216 * { data: 'a', label: 'First' },
10217 * { data: 'b', label: 'Second' },
10218 * { data: 'c', label: 'Third' }
10221 * $( document.body ).append( multiselectInput.$element );
10223 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10226 * @extends OO.ui.InputWidget
10229 * @param {Object} [config] Configuration options
10230 * @cfg {Object[]} [options=[]] Array of menu options in the format
10231 * `{ data: …, label: …, disabled: … }`
10233 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10234 // Configuration initialization
10235 config
= config
|| {};
10237 // Properties (must be done before parent constructor which calls #setDisabled)
10238 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10239 // Must be set before the #setOptionsData call below
10240 this.inputName
= config
.name
;
10241 // Set up the options before parent constructor, which uses them to validate config.value.
10242 // Use this instead of setOptions() because this.$input is not set up yet
10243 this.setOptionsData( config
.options
|| [] );
10245 // Parent constructor
10246 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10249 this.checkboxMultiselectWidget
.connect( this, {
10250 select
: 'onCheckboxesSelect'
10255 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10256 .append( this.checkboxMultiselectWidget
.$element
);
10257 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10258 this.$input
.detach();
10263 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10265 /* Static Methods */
10270 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10271 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState(
10274 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10275 .toArray().map( function ( el
) { return el
.value
; } );
10282 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10283 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10284 // Cannot reuse the `<input type=checkbox>` set
10285 delete config
.$input
;
10295 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10297 return $( '<unused>' );
10301 * Handles CheckboxMultiselectWidget select events.
10305 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10306 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10312 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10313 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10314 .toArray().map( function ( el
) { return el
.value
; } );
10315 if ( this.value
!== value
) {
10316 this.setValue( value
);
10324 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10325 value
= this.cleanUpValue( value
);
10326 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10327 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10328 if ( this.optionsDirty
) {
10329 // We reached this from the constructor or from #setOptions.
10330 // We have to update the <select> element.
10331 this.updateOptionsInterface();
10337 * Clean up incoming value.
10339 * @param {string[]} value Original value
10340 * @return {string[]} Cleaned up value
10342 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10343 var i
, singleValue
,
10345 if ( !Array
.isArray( value
) ) {
10348 for ( i
= 0; i
< value
.length
; i
++ ) {
10349 singleValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10350 .call( this, value
[ i
] );
10351 // Remove options that we don't have here
10352 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10355 cleanValue
.push( singleValue
);
10363 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10364 this.checkboxMultiselectWidget
.setDisabled( state
);
10365 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10370 * Set the options available for this input.
10372 * @param {Object[]} options Array of menu options in the format
10373 * `{ data: …, label: …, disabled: … }`
10375 * @return {OO.ui.Widget} The widget, for chaining
10377 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10378 var value
= this.getValue();
10380 this.setOptionsData( options
);
10382 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10383 // This will also get rid of any stale options that we just removed.
10384 this.setValue( value
);
10390 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10392 * This method may be called before the parent constructor, so various properties may not be
10395 * @param {Object[]} options Array of menu options in the format
10396 * `{ data: …, label: … }`
10399 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10402 this.optionsDirty
= true;
10404 this.checkboxMultiselectWidget
10406 .addItems( options
.map( function ( opt
) {
10407 var optValue
, item
, optDisabled
;
10408 optValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10409 .call( widget
, opt
.data
);
10410 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10411 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10413 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10414 disabled
: optDisabled
10416 // Set the 'name' and 'value' for form submission
10417 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10418 item
.checkbox
.setValue( optValue
);
10424 * Update the user-visible interface to match the internal list of options and value.
10426 * This method must only be called after the parent constructor.
10430 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10431 var defaultValue
= this.defaultValue
;
10433 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10434 // Remember original selection state. This property can be later used to check whether
10435 // the selection state of the input has been changed since it was created.
10436 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10437 item
.checkbox
.defaultSelected
= isDefault
;
10438 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10441 this.optionsDirty
= false;
10447 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10448 this.checkboxMultiselectWidget
.focus();
10453 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10454 * size of the field as well as its presentation. In addition, these widgets can be configured
10455 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an
10456 * optional validation-pattern (used to determine if an input value is valid or not) and an input
10457 * filter, which modifies incoming values rather than validating them.
10458 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10460 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10463 * // A TextInputWidget.
10464 * var textInput = new OO.ui.TextInputWidget( {
10465 * value: 'Text input'
10467 * $( document.body ).append( textInput.$element );
10469 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10472 * @extends OO.ui.InputWidget
10473 * @mixins OO.ui.mixin.IconElement
10474 * @mixins OO.ui.mixin.IndicatorElement
10475 * @mixins OO.ui.mixin.PendingElement
10476 * @mixins OO.ui.mixin.LabelElement
10477 * @mixins OO.ui.mixin.FlaggedElement
10480 * @param {Object} [config] Configuration options
10481 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10482 * 'email', 'url' or 'number'.
10483 * @cfg {string} [placeholder] Placeholder text
10484 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10485 * instruct the browser to focus this widget.
10486 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10487 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10489 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10490 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10491 * many emojis) count as 2 characters each.
10492 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10493 * the value or placeholder text: `'before'` or `'after'`
10494 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator:
10495 * 'required'`. Note that `false` & setting `indicator: 'required' will result in no indicator
10497 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10498 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined`
10499 * means leaving it up to the browser).
10500 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10501 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10502 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10503 * value for it to be considered valid; when Function, a function receiving the value as parameter
10504 * that must return true, or promise resolving to true, for it to be considered valid.
10506 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10507 // Configuration initialization
10508 config
= $.extend( {
10510 labelPosition
: 'after'
10513 // Parent constructor
10514 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10516 // Mixin constructors
10517 OO
.ui
.mixin
.IconElement
.call( this, config
);
10518 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10519 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( { $pending
: this.$input
}, config
) );
10520 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10521 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
10524 this.type
= this.getSaneType( config
);
10525 this.readOnly
= false;
10526 this.required
= false;
10527 this.validate
= null;
10528 this.scrollWidth
= null;
10530 this.setValidation( config
.validate
);
10531 this.setLabelPosition( config
.labelPosition
);
10535 keypress
: this.onKeyPress
.bind( this ),
10536 blur
: this.onBlur
.bind( this ),
10537 focus
: this.onFocus
.bind( this )
10539 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10540 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10541 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10542 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10546 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10547 .append( this.$icon
, this.$indicator
);
10548 this.setReadOnly( !!config
.readOnly
);
10549 this.setRequired( !!config
.required
);
10550 if ( config
.placeholder
!== undefined ) {
10551 this.$input
.attr( 'placeholder', config
.placeholder
);
10553 if ( config
.maxLength
!== undefined ) {
10554 this.$input
.attr( 'maxlength', config
.maxLength
);
10556 if ( config
.autofocus
) {
10557 this.$input
.attr( 'autofocus', 'autofocus' );
10559 if ( config
.autocomplete
=== false ) {
10560 this.$input
.attr( 'autocomplete', 'off' );
10561 // Turning off autocompletion also disables "form caching" when the user navigates to a
10562 // different page and then clicks "Back". Re-enable it when leaving.
10563 // Borrowed from jQuery UI.
10565 beforeunload: function () {
10566 this.$input
.removeAttr( 'autocomplete' );
10568 pageshow: function () {
10569 // Browsers don't seem to actually fire this event on "Back", they instead just
10570 // reload the whole page... it shouldn't hurt, though.
10571 this.$input
.attr( 'autocomplete', 'off' );
10575 if ( config
.spellcheck
!== undefined ) {
10576 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10578 if ( this.label
) {
10579 this.isWaitingToBeAttached
= true;
10580 this.installParentChangeDetector();
10586 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10587 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10588 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10589 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10590 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10591 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.FlaggedElement
);
10593 /* Static Properties */
10595 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10603 * An `enter` event is emitted when the user presses Enter key inside the text box.
10611 * Handle icon mouse down events.
10614 * @param {jQuery.Event} e Mouse down event
10615 * @return {undefined/boolean} False to prevent default if event is handled
10617 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10618 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10625 * Handle indicator mouse down events.
10628 * @param {jQuery.Event} e Mouse down event
10629 * @return {undefined/boolean} False to prevent default if event is handled
10631 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10632 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10639 * Handle key press events.
10642 * @param {jQuery.Event} e Key press event
10643 * @fires enter If Enter key is pressed
10645 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10646 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10647 this.emit( 'enter', e
);
10652 * Handle blur events.
10655 * @param {jQuery.Event} e Blur event
10657 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10658 this.setValidityFlag();
10662 * Handle focus events.
10665 * @param {jQuery.Event} e Focus event
10667 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10668 if ( this.isWaitingToBeAttached
) {
10669 // If we've received focus, then we must be attached to the document, and if
10670 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10671 this.onElementAttach();
10673 this.setValidityFlag( true );
10677 * Handle element attach events.
10680 * @param {jQuery.Event} e Element attach event
10682 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10683 this.isWaitingToBeAttached
= false;
10684 // Any previously calculated size is now probably invalid if we reattached elsewhere
10685 this.valCache
= null;
10686 this.positionLabel();
10690 * Handle debounced change events.
10692 * @param {string} value
10695 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10696 this.setValidityFlag();
10700 * Check if the input is {@link #readOnly read-only}.
10702 * @return {boolean}
10704 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10705 return this.readOnly
;
10709 * Set the {@link #readOnly read-only} state of the input.
10711 * @param {boolean} state Make input read-only
10713 * @return {OO.ui.Widget} The widget, for chaining
10715 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10716 this.readOnly
= !!state
;
10717 this.$input
.prop( 'readOnly', this.readOnly
);
10722 * Check if the input is {@link #required required}.
10724 * @return {boolean}
10726 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10727 return this.required
;
10731 * Set the {@link #required required} state of the input.
10733 * @param {boolean} state Make input required
10735 * @return {OO.ui.Widget} The widget, for chaining
10737 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10738 this.required
= !!state
;
10739 if ( this.required
) {
10741 .prop( 'required', true )
10742 .attr( 'aria-required', 'true' );
10743 if ( this.getIndicator() === null ) {
10744 this.setIndicator( 'required' );
10748 .prop( 'required', false )
10749 .removeAttr( 'aria-required' );
10750 if ( this.getIndicator() === 'required' ) {
10751 this.setIndicator( null );
10758 * Support function for making #onElementAttach work across browsers.
10760 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10761 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10763 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10764 * first time that the element gets attached to the documented.
10766 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10767 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10768 MutationObserver
= window
.MutationObserver
||
10769 window
.WebKitMutationObserver
||
10770 window
.MozMutationObserver
,
10773 if ( MutationObserver
) {
10774 // The new way. If only it wasn't so ugly.
10776 if ( this.isElementAttached() ) {
10777 // Widget is attached already, do nothing. This breaks the functionality of this
10778 // function when the widget is detached and reattached. Alas, doing this correctly with
10779 // MutationObserver would require observation of the whole document, which would hurt
10780 // performance of other, more important code.
10784 // Find topmost node in the tree
10785 topmostNode
= this.$element
[ 0 ];
10786 while ( topmostNode
.parentNode
) {
10787 topmostNode
= topmostNode
.parentNode
;
10790 // We have no way to detect the $element being attached somewhere without observing the
10791 // entire DOM with subtree modifications, which would hurt performance. So we cheat: we hook
10792 // to the parent node of $element, and instead detect when $element is removed from it (and
10793 // thus probably attached somewhere else). If there is no parent, we create a "fake" one. If
10794 // it doesn't get attached, we end up back here and create the parent.
10795 mutationObserver
= new MutationObserver( function ( mutations
) {
10796 var i
, j
, removedNodes
;
10797 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10798 removedNodes
= mutations
[ i
].removedNodes
;
10799 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10800 if ( removedNodes
[ j
] === topmostNode
) {
10801 setTimeout( onRemove
, 0 );
10808 onRemove = function () {
10809 // If the node was attached somewhere else, report it
10810 if ( widget
.isElementAttached() ) {
10811 widget
.onElementAttach();
10813 mutationObserver
.disconnect();
10814 widget
.installParentChangeDetector();
10817 // Create a fake parent and observe it
10818 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10819 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10821 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10822 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10823 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10831 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10832 if ( this.getSaneType( config
) === 'number' ) {
10833 return $( '<input>' )
10834 .attr( 'step', 'any' )
10835 .attr( 'type', 'number' );
10837 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10842 * Get sanitized value for 'type' for given config.
10844 * @param {Object} config Configuration options
10845 * @return {string|null}
10848 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10849 var allowedTypes
= [
10856 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10860 * Focus the input and select a specified range within the text.
10862 * @param {number} from Select from offset
10863 * @param {number} [to] Select to offset, defaults to from
10865 * @return {OO.ui.Widget} The widget, for chaining
10867 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10868 var isBackwards
, start
, end
,
10869 input
= this.$input
[ 0 ];
10873 isBackwards
= to
< from;
10874 start
= isBackwards
? to
: from;
10875 end
= isBackwards
? from : to
;
10880 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10882 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10883 // Rather than expensively check if the input is attached every time, just check
10884 // if it was the cause of an error being thrown. If not, rethrow the error.
10885 if ( this.getElementDocument().body
.contains( input
) ) {
10893 * Get an object describing the current selection range in a directional manner
10895 * @return {Object} Object containing 'from' and 'to' offsets
10897 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10898 var input
= this.$input
[ 0 ],
10899 start
= input
.selectionStart
,
10900 end
= input
.selectionEnd
,
10901 isBackwards
= input
.selectionDirection
=== 'backward';
10904 from: isBackwards
? end
: start
,
10905 to
: isBackwards
? start
: end
10910 * Get the length of the text input value.
10912 * This could differ from the length of #getValue if the
10913 * value gets filtered
10915 * @return {number} Input length
10917 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10918 return this.$input
[ 0 ].value
.length
;
10922 * Focus the input and select the entire text.
10925 * @return {OO.ui.Widget} The widget, for chaining
10927 OO
.ui
.TextInputWidget
.prototype.select = function () {
10928 return this.selectRange( 0, this.getInputLength() );
10932 * Focus the input and move the cursor to the start.
10935 * @return {OO.ui.Widget} The widget, for chaining
10937 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10938 return this.selectRange( 0 );
10942 * Focus the input and move the cursor to the end.
10945 * @return {OO.ui.Widget} The widget, for chaining
10947 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10948 return this.selectRange( this.getInputLength() );
10952 * Insert new content into the input.
10954 * @param {string} content Content to be inserted
10956 * @return {OO.ui.Widget} The widget, for chaining
10958 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10960 range
= this.getRange(),
10961 value
= this.getValue();
10963 start
= Math
.min( range
.from, range
.to
);
10964 end
= Math
.max( range
.from, range
.to
);
10966 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10967 this.selectRange( start
+ content
.length
);
10972 * Insert new content either side of a selection.
10974 * @param {string} pre Content to be inserted before the selection
10975 * @param {string} post Content to be inserted after the selection
10977 * @return {OO.ui.Widget} The widget, for chaining
10979 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10981 range
= this.getRange(),
10982 offset
= pre
.length
;
10984 start
= Math
.min( range
.from, range
.to
);
10985 end
= Math
.max( range
.from, range
.to
);
10987 this.selectRange( start
).insertContent( pre
);
10988 this.selectRange( offset
+ end
).insertContent( post
);
10990 this.selectRange( offset
+ start
, offset
+ end
);
10995 * Set the validation pattern.
10997 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10998 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10999 * value must contain only numbers).
11001 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
11002 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11004 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
11005 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
11006 this.validate
= validate
;
11008 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
11013 * Sets the 'invalid' flag appropriately.
11015 * @param {boolean} [isValid] Optionally override validation result
11017 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
11019 setFlag = function ( valid
) {
11021 widget
.$input
.attr( 'aria-invalid', 'true' );
11023 widget
.$input
.removeAttr( 'aria-invalid' );
11025 widget
.setFlags( { invalid
: !valid
} );
11028 if ( isValid
!== undefined ) {
11029 setFlag( isValid
);
11031 this.getValidity().then( function () {
11040 * Get the validity of current value.
11042 * This method returns a promise that resolves if the value is valid and rejects if
11043 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
11045 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
11047 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
11050 function rejectOrResolve( valid
) {
11052 return $.Deferred().resolve().promise();
11054 return $.Deferred().reject().promise();
11058 // Check browser validity and reject if it is invalid
11060 this.$input
[ 0 ].checkValidity
!== undefined &&
11061 this.$input
[ 0 ].checkValidity() === false
11063 return rejectOrResolve( false );
11066 // Run our checks if the browser thinks the field is valid
11067 if ( this.validate
instanceof Function
) {
11068 result
= this.validate( this.getValue() );
11069 if ( result
&& typeof result
.promise
=== 'function' ) {
11070 return result
.promise().then( function ( valid
) {
11071 return rejectOrResolve( valid
);
11074 return rejectOrResolve( result
);
11077 return rejectOrResolve( this.getValue().match( this.validate
) );
11082 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11084 * @param {string} labelPosition Label position, 'before' or 'after'
11086 * @return {OO.ui.Widget} The widget, for chaining
11088 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
11089 this.labelPosition
= labelPosition
;
11090 if ( this.label
) {
11091 // If there is no label and we only change the position, #updatePosition is a no-op,
11092 // but it takes really a lot of work to do nothing.
11093 this.updatePosition();
11099 * Update the position of the inline label.
11101 * This method is called by #setLabelPosition, and can also be called on its own if
11102 * something causes the label to be mispositioned.
11105 * @return {OO.ui.Widget} The widget, for chaining
11107 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
11108 var after
= this.labelPosition
=== 'after';
11111 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
11112 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
11114 this.valCache
= null;
11115 this.scrollWidth
= null;
11116 this.positionLabel();
11122 * Position the label by setting the correct padding on the input.
11126 * @return {OO.ui.Widget} The widget, for chaining
11128 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11129 var after
, rtl
, property
, newCss
;
11131 if ( this.isWaitingToBeAttached
) {
11132 // #onElementAttach will be called soon, which calls this method
11137 'padding-right': '',
11141 if ( this.label
) {
11142 this.$element
.append( this.$label
);
11144 this.$label
.detach();
11145 // Clear old values if present
11146 this.$input
.css( newCss
);
11150 after
= this.labelPosition
=== 'after';
11151 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11152 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11154 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11155 // We have to clear the padding on the other side, in case the element direction changed
11156 this.$input
.css( newCss
);
11162 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11163 * {@link OO.ui.mixin.IconElement search icon} by default.
11164 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11166 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11169 * @extends OO.ui.TextInputWidget
11172 * @param {Object} [config] Configuration options
11174 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11175 config
= $.extend( {
11179 // Parent constructor
11180 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11183 this.connect( this, {
11186 this.$indicator
.on( 'click', this.onIndicatorClick
.bind( this ) );
11189 this.updateSearchIndicator();
11190 this.connect( this, {
11191 disable
: 'onDisable'
11197 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11205 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11210 * Handle click events on the indicator
11212 * @param {jQuery.Event} e Click event
11213 * @return {boolean}
11215 OO
.ui
.SearchInputWidget
.prototype.onIndicatorClick = function ( e
) {
11216 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11217 // Clear the text field
11218 this.setValue( '' );
11225 * Update the 'clear' indicator displayed on type: 'search' text
11226 * fields, hiding it when the field is already empty or when it's not
11229 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11230 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11231 this.setIndicator( null );
11233 this.setIndicator( 'clear' );
11238 * Handle change events.
11242 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11243 this.updateSearchIndicator();
11247 * Handle disable events.
11249 * @param {boolean} disabled Element is disabled
11252 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11253 this.updateSearchIndicator();
11259 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11260 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11261 this.updateSearchIndicator();
11266 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11267 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11268 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11269 * {@link OO.ui.mixin.IndicatorElement indicators}.
11270 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11272 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11275 * // A MultilineTextInputWidget.
11276 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11277 * value: 'Text input on multiple lines'
11279 * $( document.body ).append( multilineTextInput.$element );
11281 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11284 * @extends OO.ui.TextInputWidget
11287 * @param {Object} [config] Configuration options
11288 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11289 * specifies minimum number of rows to display.
11290 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11291 * Use the #maxRows config to specify a maximum number of displayed rows.
11292 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11293 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11295 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11296 config
= $.extend( {
11299 // Parent constructor
11300 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11303 this.autosize
= !!config
.autosize
;
11304 this.styleHeight
= null;
11305 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11306 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11308 // Clone for resizing
11309 if ( this.autosize
) {
11310 this.$clone
= this.$input
11312 .removeAttr( 'id' )
11313 .removeAttr( 'name' )
11314 .insertAfter( this.$input
)
11315 .attr( 'aria-hidden', 'true' )
11316 .addClass( 'oo-ui-element-hidden' );
11320 this.connect( this, {
11325 if ( config
.rows
) {
11326 this.$input
.attr( 'rows', config
.rows
);
11328 if ( this.autosize
) {
11329 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11330 this.isWaitingToBeAttached
= true;
11331 this.installParentChangeDetector();
11337 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11339 /* Static Methods */
11344 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11345 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11346 state
.scrollTop
= config
.$input
.scrollTop();
11355 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11356 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11361 * Handle change events.
11365 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11372 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11373 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11380 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11382 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11384 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11385 // Some platforms emit keycode 10 for Control+Enter keypress in a textarea
11388 this.emit( 'enter', e
);
11393 * Automatically adjust the size of the text input.
11395 * This only affects multiline inputs that are {@link #autosize autosized}.
11398 * @return {OO.ui.Widget} The widget, for chaining
11401 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11402 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11403 idealHeight
, newHeight
, scrollWidth
, property
;
11405 if ( this.$input
.val() !== this.valCache
) {
11406 if ( this.autosize
) {
11408 .val( this.$input
.val() )
11409 .attr( 'rows', this.minRows
)
11410 // Set inline height property to 0 to measure scroll height
11411 .css( 'height', 0 );
11413 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11415 this.valCache
= this.$input
.val();
11417 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11419 // Remove inline height property to measure natural heights
11420 this.$clone
.css( 'height', '' );
11421 innerHeight
= this.$clone
.innerHeight();
11422 outerHeight
= this.$clone
.outerHeight();
11424 // Measure max rows height
11426 .attr( 'rows', this.maxRows
)
11427 .css( 'height', 'auto' )
11429 maxInnerHeight
= this.$clone
.innerHeight();
11431 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11432 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11433 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11434 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11436 this.$clone
.addClass( 'oo-ui-element-hidden' );
11438 // Only apply inline height when expansion beyond natural height is needed
11439 // Use the difference between the inner and outer height as a buffer
11440 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11441 if ( newHeight
!== this.styleHeight
) {
11442 this.$input
.css( 'height', newHeight
);
11443 this.styleHeight
= newHeight
;
11444 this.emit( 'resize' );
11447 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11448 if ( scrollWidth
!== this.scrollWidth
) {
11449 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11451 this.$label
.css( { right
: '', left
: '' } );
11452 this.$indicator
.css( { right
: '', left
: '' } );
11454 if ( scrollWidth
) {
11455 this.$indicator
.css( property
, scrollWidth
);
11456 if ( this.labelPosition
=== 'after' ) {
11457 this.$label
.css( property
, scrollWidth
);
11461 this.scrollWidth
= scrollWidth
;
11462 this.positionLabel();
11472 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11473 return $( '<textarea>' );
11477 * Check if the input automatically adjusts its size.
11479 * @return {boolean}
11481 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11482 return !!this.autosize
;
11488 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11489 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11490 if ( state
.scrollTop
!== undefined ) {
11491 this.$input
.scrollTop( state
.scrollTop
);
11496 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11497 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11498 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11500 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11501 * option, that option will appear to be selected.
11502 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11505 * After the user chooses an option, its `data` will be used as a new value for the widget.
11506 * A `label` also can be specified for each option: if given, it will be shown instead of the
11507 * `data` in the dropdown menu.
11509 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11511 * For more information about menus and options, please see the
11512 * [OOUI documentation on MediaWiki][1].
11515 * // A ComboBoxInputWidget.
11516 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11517 * value: 'Option 1',
11519 * { data: 'Option 1' },
11520 * { data: 'Option 2' },
11521 * { data: 'Option 3' }
11524 * $( document.body ).append( comboBox.$element );
11527 * // Example: A ComboBoxInputWidget with additional option labels.
11528 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11529 * value: 'Option 1',
11532 * data: 'Option 1',
11533 * label: 'Option One'
11536 * data: 'Option 2',
11537 * label: 'Option Two'
11540 * data: 'Option 3',
11541 * label: 'Option Three'
11545 * $( document.body ).append( comboBox.$element );
11547 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11550 * @extends OO.ui.TextInputWidget
11553 * @param {Object} [config] Configuration options
11554 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11555 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu
11557 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
11558 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
11559 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
11560 * uses relative positioning.
11561 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11563 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11564 // Configuration initialization
11565 config
= $.extend( {
11566 autocomplete
: false
11569 // ComboBoxInputWidget shouldn't support `multiline`
11570 config
.multiline
= false;
11572 // See InputWidget#reusePreInfuseDOM about `config.$input`
11573 if ( config
.$input
) {
11574 config
.$input
.removeAttr( 'list' );
11577 // Parent constructor
11578 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11581 this.$overlay
= ( config
.$overlay
=== true ?
11582 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11583 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11584 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11585 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11587 invisibleLabel
: true,
11588 disabled
: this.disabled
11590 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11594 $floatableContainer
: this.$element
,
11595 disabled
: this.isDisabled()
11601 this.connect( this, {
11602 change
: 'onInputChange',
11603 enter
: 'onInputEnter'
11605 this.dropdownButton
.connect( this, {
11606 click
: 'onDropdownButtonClick'
11608 this.menu
.connect( this, {
11609 choose
: 'onMenuChoose',
11610 add
: 'onMenuItemsChange',
11611 remove
: 'onMenuItemsChange',
11612 toggle
: 'onMenuToggle'
11616 this.$input
.attr( {
11618 'aria-owns': this.menu
.getElementId(),
11619 'aria-autocomplete': 'list'
11621 this.dropdownButton
.$button
.attr( {
11622 'aria-controls': this.menu
.getElementId()
11624 // Do not override options set via config.menu.items
11625 if ( config
.options
!== undefined ) {
11626 this.setOptions( config
.options
);
11628 this.$field
= $( '<div>' )
11629 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11630 .append( this.$input
, this.dropdownButton
.$element
);
11632 .addClass( 'oo-ui-comboBoxInputWidget' )
11633 .append( this.$field
);
11634 this.$overlay
.append( this.menu
.$element
);
11635 this.onMenuItemsChange();
11640 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11645 * Get the combobox's menu.
11647 * @return {OO.ui.MenuSelectWidget} Menu widget
11649 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11654 * Get the combobox's text input widget.
11656 * @return {OO.ui.TextInputWidget} Text input widget
11658 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11663 * Handle input change events.
11666 * @param {string} value New value
11668 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11669 var match
= this.menu
.findItemFromData( value
);
11671 this.menu
.selectItem( match
);
11672 if ( this.menu
.findHighlightedItem() ) {
11673 this.menu
.highlightItem( match
);
11676 if ( !this.isDisabled() ) {
11677 this.menu
.toggle( true );
11682 * Handle input enter events.
11686 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11687 if ( !this.isDisabled() ) {
11688 this.menu
.toggle( false );
11693 * Handle button click events.
11697 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11698 this.menu
.toggle();
11703 * Handle menu choose events.
11706 * @param {OO.ui.OptionWidget} item Chosen item
11708 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11709 this.setValue( item
.getData() );
11713 * Handle menu item change events.
11717 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11718 var match
= this.menu
.findItemFromData( this.getValue() );
11719 this.menu
.selectItem( match
);
11720 if ( this.menu
.findHighlightedItem() ) {
11721 this.menu
.highlightItem( match
);
11723 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11727 * Handle menu toggle events.
11730 * @param {boolean} isVisible Open state of the menu
11732 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11733 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11737 * Update the disabled state of the controls
11741 * @return {OO.ui.ComboBoxInputWidget} The widget, for chaining
11743 OO
.ui
.ComboBoxInputWidget
.prototype.updateControlsDisabled = function () {
11744 var disabled
= this.isDisabled() || this.isReadOnly();
11745 if ( this.dropdownButton
) {
11746 this.dropdownButton
.setDisabled( disabled
);
11749 this.menu
.setDisabled( disabled
);
11757 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function () {
11759 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.apply( this, arguments
);
11760 this.updateControlsDisabled();
11767 OO
.ui
.ComboBoxInputWidget
.prototype.setReadOnly = function () {
11769 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
11770 this.updateControlsDisabled();
11775 * Set the options available for this input.
11777 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11779 * @return {OO.ui.Widget} The widget, for chaining
11781 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11784 .addItems( options
.map( function ( opt
) {
11785 return new OO
.ui
.MenuOptionWidget( {
11787 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11795 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11796 * which is a widget that is specified by reference before any optional configuration settings.
11798 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of
11801 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11802 * A left-alignment is used for forms with many fields.
11803 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11804 * A right-alignment is used for long but familiar forms which users tab through,
11805 * verifying the current field with a quick glance at the label.
11806 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11807 * that users fill out from top to bottom.
11808 * - **inline**: The label is placed after the field-widget and aligned to the left.
11809 * An inline-alignment is best used with checkboxes or radio buttons.
11811 * Help text can either be:
11813 * - accessed via a help icon that appears in the upper right corner of the rendered field layout,
11815 * - shown as a subtle explanation below the label.
11817 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`.
11818 * If it is long or not essential, leave `helpInline` to its default, `false`.
11820 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11822 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11825 * @extends OO.ui.Layout
11826 * @mixins OO.ui.mixin.LabelElement
11827 * @mixins OO.ui.mixin.TitledElement
11830 * @param {OO.ui.Widget} fieldWidget Field widget
11831 * @param {Object} [config] Configuration options
11832 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11834 * @cfg {Array} [errors] Error messages about the widget, which will be
11835 * displayed below the widget.
11836 * @cfg {Array} [warnings] Warning messages about the widget, which will be
11837 * displayed below the widget.
11838 * @cfg {Array} [successMessages] Success messages on user interactions with the widget,
11839 * which will be displayed below the widget.
11840 * The array may contain strings or OO.ui.HtmlSnippet instances.
11841 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11842 * below the widget.
11843 * The array may contain strings or OO.ui.HtmlSnippet instances.
11844 * These are more visible than `help` messages when `helpInline` is set, and so
11845 * might be good for transient messages.
11846 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11847 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11848 * corner of the rendered field; clicking it will display the text in a popup.
11849 * If `helpInline` is `true`, then a subtle description will be shown after the
11851 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11852 * or shown when the "help" icon is clicked.
11853 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11855 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11857 * @throws {Error} An error is thrown if no widget is specified
11859 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11860 // Allow passing positional parameters inside the config object
11861 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11862 config
= fieldWidget
;
11863 fieldWidget
= config
.fieldWidget
;
11866 // Make sure we have required constructor arguments
11867 if ( fieldWidget
=== undefined ) {
11868 throw new Error( 'Widget not found' );
11871 // Configuration initialization
11872 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11874 // Parent constructor
11875 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11877 // Mixin constructors
11878 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {
11879 $label
: $( '<label>' )
11881 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( { $titled
: this.$label
}, config
) );
11884 this.fieldWidget
= fieldWidget
;
11886 this.warnings
= [];
11887 this.successMessages
= [];
11889 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11890 this.$messages
= $( '<ul>' );
11891 this.$header
= $( '<span>' );
11892 this.$body
= $( '<div>' );
11894 this.helpInline
= config
.helpInline
;
11897 this.fieldWidget
.connect( this, {
11898 disable
: 'onFieldDisable'
11902 this.$help
= config
.help
?
11903 this.createHelpElement( config
.help
, config
.$overlay
) :
11905 if ( this.fieldWidget
.getInputId() ) {
11906 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11907 if ( this.helpInline
) {
11908 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
11911 this.$label
.on( 'click', function () {
11912 this.fieldWidget
.simulateLabelClick();
11914 if ( this.helpInline
) {
11915 this.$help
.on( 'click', function () {
11916 this.fieldWidget
.simulateLabelClick();
11921 .addClass( 'oo-ui-fieldLayout' )
11922 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11923 .append( this.$body
);
11924 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11925 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11926 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11928 .addClass( 'oo-ui-fieldLayout-field' )
11929 .append( this.fieldWidget
.$element
);
11931 this.setErrors( config
.errors
|| [] );
11932 this.setWarnings( config
.warnings
|| [] );
11933 this.setSuccess( config
.successMessages
|| [] );
11934 this.setNotices( config
.notices
|| [] );
11935 this.setAlignment( config
.align
);
11936 // Call this again to take into account the widget's accessKey
11937 this.updateTitle();
11942 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11943 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11944 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11949 * Handle field disable events.
11952 * @param {boolean} value Field is disabled
11954 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11955 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11959 * Get the widget contained by the field.
11961 * @return {OO.ui.Widget} Field widget
11963 OO
.ui
.FieldLayout
.prototype.getField = function () {
11964 return this.fieldWidget
;
11968 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11969 * #setAlignment). Return `false` if it can't or if this can't be determined.
11971 * @return {boolean}
11973 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11974 // This is very simplistic, but should be good enough.
11975 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11980 * @param {string} kind 'error' or 'notice'
11981 * @param {string|OO.ui.HtmlSnippet} text
11984 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11985 var $listItem
, $icon
, message
;
11986 $listItem
= $( '<li>' );
11987 if ( kind
=== 'error' ) {
11988 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'error' ] } ).$element
;
11989 $listItem
.attr( 'role', 'alert' );
11990 } else if ( kind
=== 'warning' ) {
11991 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11992 $listItem
.attr( 'role', 'alert' );
11993 } else if ( kind
=== 'success' ) {
11994 $icon
= new OO
.ui
.IconWidget( { icon
: 'check', flags
: [ 'success' ] } ).$element
;
11995 } else if ( kind
=== 'notice' ) {
11996 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
12000 message
= new OO
.ui
.LabelWidget( { label
: text
} );
12002 .append( $icon
, message
.$element
)
12003 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
12008 * Set the field alignment mode.
12011 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
12013 * @return {OO.ui.BookletLayout} The layout, for chaining
12015 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
12016 if ( value
!== this.align
) {
12017 // Default to 'left'
12018 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
12022 if ( value
=== 'inline' && !this.isFieldInline() ) {
12025 // Reorder elements
12027 if ( this.helpInline
) {
12028 if ( value
=== 'top' ) {
12029 this.$header
.append( this.$label
);
12030 this.$body
.append( this.$header
, this.$field
, this.$help
);
12031 } else if ( value
=== 'inline' ) {
12032 this.$header
.append( this.$label
, this.$help
);
12033 this.$body
.append( this.$field
, this.$header
);
12035 this.$header
.append( this.$label
, this.$help
);
12036 this.$body
.append( this.$header
, this.$field
);
12039 if ( value
=== 'top' ) {
12040 this.$header
.append( this.$help
, this.$label
);
12041 this.$body
.append( this.$header
, this.$field
);
12042 } else if ( value
=== 'inline' ) {
12043 this.$header
.append( this.$help
, this.$label
);
12044 this.$body
.append( this.$field
, this.$header
);
12046 this.$header
.append( this.$label
);
12047 this.$body
.append( this.$header
, this.$help
, this.$field
);
12050 // Set classes. The following classes can be used here:
12051 // * oo-ui-fieldLayout-align-left
12052 // * oo-ui-fieldLayout-align-right
12053 // * oo-ui-fieldLayout-align-top
12054 // * oo-ui-fieldLayout-align-inline
12055 if ( this.align
) {
12056 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
12058 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
12059 this.align
= value
;
12066 * Set the list of error messages.
12068 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
12069 * The array may contain strings or OO.ui.HtmlSnippet instances.
12071 * @return {OO.ui.BookletLayout} The layout, for chaining
12073 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
12074 this.errors
= errors
.slice();
12075 this.updateMessages();
12080 * Set the list of warning messages.
12082 * @param {Array} warnings Warning messages about the widget, which will be displayed below
12084 * The array may contain strings or OO.ui.HtmlSnippet instances.
12086 * @return {OO.ui.BookletLayout} The layout, for chaining
12088 OO
.ui
.FieldLayout
.prototype.setWarnings = function ( warnings
) {
12089 this.warnings
= warnings
.slice();
12090 this.updateMessages();
12095 * Set the list of success messages.
12097 * @param {Array} successMessages Success messages about the widget, which will be displayed below
12099 * The array may contain strings or OO.ui.HtmlSnippet instances.
12101 * @return {OO.ui.BookletLayout} The layout, for chaining
12103 OO
.ui
.FieldLayout
.prototype.setSuccess = function ( successMessages
) {
12104 this.successMessages
= successMessages
.slice();
12105 this.updateMessages();
12110 * Set the list of notice messages.
12112 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
12113 * The array may contain strings or OO.ui.HtmlSnippet instances.
12115 * @return {OO.ui.BookletLayout} The layout, for chaining
12117 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
12118 this.notices
= notices
.slice();
12119 this.updateMessages();
12124 * Update the rendering of error, warning, success and notice messages.
12128 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
12130 this.$messages
.empty();
12133 this.errors
.length
||
12134 this.warnings
.length
||
12135 this.successMessages
.length
||
12136 this.notices
.length
12138 this.$body
.after( this.$messages
);
12140 this.$messages
.remove();
12144 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
12145 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
12147 for ( i
= 0; i
< this.warnings
.length
; i
++ ) {
12148 this.$messages
.append( this.makeMessage( 'warning', this.warnings
[ i
] ) );
12150 for ( i
= 0; i
< this.successMessages
.length
; i
++ ) {
12151 this.$messages
.append( this.makeMessage( 'success', this.successMessages
[ i
] ) );
12153 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
12154 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
12159 * Include information about the widget's accessKey in our title. TitledElement calls this method.
12160 * (This is a bit of a hack.)
12163 * @param {string} title Tooltip label for 'title' attribute
12166 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
12167 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
12168 return this.fieldWidget
.formatTitleWithAccessKey( title
);
12174 * Creates and returns the help element. Also sets the `aria-describedby`
12175 * attribute on the main element of the `fieldWidget`.
12178 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
12179 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
12180 * @return {jQuery} The element that should become `this.$help`.
12182 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
12183 var helpId
, helpWidget
;
12185 if ( this.helpInline
) {
12186 helpWidget
= new OO
.ui
.LabelWidget( {
12188 classes
: [ 'oo-ui-inline-help' ]
12191 helpId
= helpWidget
.getElementId();
12193 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12194 $overlay
: $overlay
,
12198 classes
: [ 'oo-ui-fieldLayout-help' ],
12201 label
: OO
.ui
.msg( 'ooui-field-help' ),
12202 invisibleLabel
: true
12204 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
12205 helpWidget
.getPopup().$body
.html( help
.toString() );
12207 helpWidget
.getPopup().$body
.text( help
);
12210 helpId
= helpWidget
.getPopup().getBodyId();
12213 // Set the 'aria-describedby' attribute on the fieldWidget
12214 // Preference given to an input or a button
12216 this.fieldWidget
.$input
||
12217 this.fieldWidget
.$button
||
12218 this.fieldWidget
.$element
12219 ).attr( 'aria-describedby', helpId
);
12221 return helpWidget
.$element
;
12225 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget,
12226 * a button, and an optional label and/or help text. The field-widget (e.g., a
12227 * {@link OO.ui.TextInputWidget TextInputWidget}), is required and is specified before any optional
12228 * configuration settings.
12230 * Labels can be aligned in one of four ways:
12232 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12233 * A left-alignment is used for forms with many fields.
12234 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12235 * A right-alignment is used for long but familiar forms which users tab through,
12236 * verifying the current field with a quick glance at the label.
12237 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12238 * that users fill out from top to bottom.
12239 * - **inline**: The label is placed after the field-widget and aligned to the left.
12240 * An inline-alignment is best used with checkboxes or radio buttons.
12242 * Help text is accessed via a help icon that appears in the upper right corner of the rendered
12243 * field layout when help text is specified.
12246 * // Example of an ActionFieldLayout
12247 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12248 * new OO.ui.TextInputWidget( {
12249 * placeholder: 'Field widget'
12251 * new OO.ui.ButtonWidget( {
12255 * label: 'An ActionFieldLayout. This label is aligned top',
12257 * help: 'This is help text'
12261 * $( document.body ).append( actionFieldLayout.$element );
12264 * @extends OO.ui.FieldLayout
12267 * @param {OO.ui.Widget} fieldWidget Field widget
12268 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12269 * @param {Object} config
12271 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12272 // Allow passing positional parameters inside the config object
12273 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12274 config
= fieldWidget
;
12275 fieldWidget
= config
.fieldWidget
;
12276 buttonWidget
= config
.buttonWidget
;
12279 // Parent constructor
12280 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12283 this.buttonWidget
= buttonWidget
;
12284 this.$button
= $( '<span>' );
12285 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12288 this.$element
.addClass( 'oo-ui-actionFieldLayout' );
12290 .addClass( 'oo-ui-actionFieldLayout-button' )
12291 .append( this.buttonWidget
.$element
);
12293 .addClass( 'oo-ui-actionFieldLayout-input' )
12294 .append( this.fieldWidget
.$element
);
12295 this.$field
.append( this.$input
, this.$button
);
12300 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12303 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12304 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12305 * configured with a label as well. For more information and examples,
12306 * please see the [OOUI documentation on MediaWiki][1].
12309 * // Example of a fieldset layout
12310 * var input1 = new OO.ui.TextInputWidget( {
12311 * placeholder: 'A text input field'
12314 * var input2 = new OO.ui.TextInputWidget( {
12315 * placeholder: 'A text input field'
12318 * var fieldset = new OO.ui.FieldsetLayout( {
12319 * label: 'Example of a fieldset layout'
12322 * fieldset.addItems( [
12323 * new OO.ui.FieldLayout( input1, {
12324 * label: 'Field One'
12326 * new OO.ui.FieldLayout( input2, {
12327 * label: 'Field Two'
12330 * $( document.body ).append( fieldset.$element );
12332 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12335 * @extends OO.ui.Layout
12336 * @mixins OO.ui.mixin.IconElement
12337 * @mixins OO.ui.mixin.LabelElement
12338 * @mixins OO.ui.mixin.GroupElement
12341 * @param {Object} [config] Configuration options
12342 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset.
12343 * See OO.ui.FieldLayout for more information about fields.
12344 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon
12345 * will appear in the upper-right corner of the rendered field; clicking it will display the text
12346 * in a popup. For important messages, you are advised to use `notices`, as they are always shown.
12347 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12348 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12350 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12351 // Configuration initialization
12352 config
= config
|| {};
12354 // Parent constructor
12355 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12357 // Mixin constructors
12358 OO
.ui
.mixin
.IconElement
.call( this, config
);
12359 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12360 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12363 this.$header
= $( '<legend>' );
12364 if ( config
.help
) {
12365 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
12366 $overlay
: config
.$overlay
,
12370 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12373 label
: OO
.ui
.msg( 'ooui-field-help' ),
12374 invisibleLabel
: true
12376 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12377 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
12379 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
12381 this.$help
= this.popupButtonWidget
.$element
;
12383 this.$help
= $( [] );
12388 .addClass( 'oo-ui-fieldsetLayout-header' )
12389 .append( this.$icon
, this.$label
, this.$help
);
12390 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12392 .addClass( 'oo-ui-fieldsetLayout' )
12393 .prepend( this.$header
, this.$group
);
12394 if ( Array
.isArray( config
.items
) ) {
12395 this.addItems( config
.items
);
12401 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12402 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12403 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12404 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12406 /* Static Properties */
12412 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12415 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use
12416 * browser-based form submission for the fields instead of handling them in JavaScript. Form layouts
12417 * can be configured with an HTML form action, an encoding type, and a method using the #action,
12418 * #enctype, and #method configs, respectively.
12419 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12421 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12422 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12423 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12424 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12425 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12426 * often have simplified APIs to match the capabilities of HTML forms.
12427 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12429 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12430 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12433 * // Example of a form layout that wraps a fieldset layout.
12434 * var input1 = new OO.ui.TextInputWidget( {
12435 * placeholder: 'Username'
12437 * input2 = new OO.ui.TextInputWidget( {
12438 * placeholder: 'Password',
12441 * submit = new OO.ui.ButtonInputWidget( {
12444 * fieldset = new OO.ui.FieldsetLayout( {
12445 * label: 'A form layout'
12448 * fieldset.addItems( [
12449 * new OO.ui.FieldLayout( input1, {
12450 * label: 'Username',
12453 * new OO.ui.FieldLayout( input2, {
12454 * label: 'Password',
12457 * new OO.ui.FieldLayout( submit )
12459 * var form = new OO.ui.FormLayout( {
12460 * items: [ fieldset ],
12461 * action: '/api/formhandler',
12464 * $( document.body ).append( form.$element );
12467 * @extends OO.ui.Layout
12468 * @mixins OO.ui.mixin.GroupElement
12471 * @param {Object} [config] Configuration options
12472 * @cfg {string} [method] HTML form `method` attribute
12473 * @cfg {string} [action] HTML form `action` attribute
12474 * @cfg {string} [enctype] HTML form `enctype` attribute
12475 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12477 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12480 // Configuration initialization
12481 config
= config
|| {};
12483 // Parent constructor
12484 OO
.ui
.FormLayout
.parent
.call( this, config
);
12486 // Mixin constructors
12487 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12490 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12492 // Make sure the action is safe
12493 action
= config
.action
;
12494 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12495 action
= './' + action
;
12500 .addClass( 'oo-ui-formLayout' )
12502 method
: config
.method
,
12504 enctype
: config
.enctype
12506 if ( Array
.isArray( config
.items
) ) {
12507 this.addItems( config
.items
);
12513 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12514 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12519 * A 'submit' event is emitted when the form is submitted.
12524 /* Static Properties */
12530 OO
.ui
.FormLayout
.static.tagName
= 'form';
12535 * Handle form submit events.
12538 * @param {jQuery.Event} e Submit event
12540 * @return {OO.ui.FormLayout} The layout, for chaining
12542 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12543 if ( this.emit( 'submit' ) ) {
12549 * PanelLayouts expand to cover the entire area of their parent. They can be configured with
12550 * scrolling, padding, and a frame, and are often used together with
12551 * {@link OO.ui.StackLayout StackLayouts}.
12554 * // Example of a panel layout
12555 * var panel = new OO.ui.PanelLayout( {
12559 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12561 * $( document.body ).append( panel.$element );
12564 * @extends OO.ui.Layout
12567 * @param {Object} [config] Configuration options
12568 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12569 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12570 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12571 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside
12574 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12575 // Configuration initialization
12576 config
= $.extend( {
12583 // Parent constructor
12584 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12587 this.$element
.addClass( 'oo-ui-panelLayout' );
12588 if ( config
.scrollable
) {
12589 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12591 if ( config
.padded
) {
12592 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12594 if ( config
.expanded
) {
12595 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12597 if ( config
.framed
) {
12598 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12604 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12606 /* Static Methods */
12611 OO
.ui
.PanelLayout
.static.reusePreInfuseDOM = function ( node
, config
) {
12612 config
= OO
.ui
.PanelLayout
.parent
.static.reusePreInfuseDOM( node
, config
);
12613 if ( config
.preserveContent
!== false ) {
12614 config
.$content
= $( node
).contents();
12622 * Focus the panel layout
12624 * The default implementation just focuses the first focusable element in the panel
12626 OO
.ui
.PanelLayout
.prototype.focus = function () {
12627 OO
.ui
.findFocusable( this.$element
).focus();
12631 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12632 * items), with small margins between them. Convenient when you need to put a number of block-level
12633 * widgets on a single line next to each other.
12635 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12638 * // HorizontalLayout with a text input and a label.
12639 * var layout = new OO.ui.HorizontalLayout( {
12641 * new OO.ui.LabelWidget( { label: 'Label' } ),
12642 * new OO.ui.TextInputWidget( { value: 'Text' } )
12645 * $( document.body ).append( layout.$element );
12648 * @extends OO.ui.Layout
12649 * @mixins OO.ui.mixin.GroupElement
12652 * @param {Object} [config] Configuration options
12653 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12655 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12656 // Configuration initialization
12657 config
= config
|| {};
12659 // Parent constructor
12660 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12662 // Mixin constructors
12663 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( { $group
: this.$element
}, config
) );
12666 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12667 if ( Array
.isArray( config
.items
) ) {
12668 this.addItems( config
.items
);
12674 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12675 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12678 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12679 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12680 * (to adjust the value in increments) to allow the user to enter a number.
12683 * // A NumberInputWidget.
12684 * var numberInput = new OO.ui.NumberInputWidget( {
12685 * label: 'NumberInputWidget',
12686 * input: { value: 5 },
12690 * $( document.body ).append( numberInput.$element );
12693 * @extends OO.ui.TextInputWidget
12696 * @param {Object} [config] Configuration options
12697 * @cfg {Object} [minusButton] Configuration options to pass to the
12698 * {@link OO.ui.ButtonWidget decrementing button widget}.
12699 * @cfg {Object} [plusButton] Configuration options to pass to the
12700 * {@link OO.ui.ButtonWidget incrementing button widget}.
12701 * @cfg {number} [min=-Infinity] Minimum allowed value
12702 * @cfg {number} [max=Infinity] Maximum allowed value
12703 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12704 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or Up/Down arrow keys.
12705 * Defaults to `step` if specified, otherwise `1`.
12706 * @cfg {number} [pageStep=10*buttonStep] Delta when using the Page-up/Page-down keys.
12707 * Defaults to 10 times `buttonStep`.
12708 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12710 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12711 var $field
= $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' );
12713 // Configuration initialization
12714 config
= $.extend( {
12720 // For backward compatibility
12721 $.extend( config
, config
.input
);
12724 // Parent constructor
12725 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12729 if ( config
.showButtons
) {
12730 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12732 disabled
: this.isDisabled(),
12734 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12739 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12740 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12742 disabled
: this.isDisabled(),
12744 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12749 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12754 keydown
: this.onKeyDown
.bind( this ),
12755 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12757 if ( config
.showButtons
) {
12758 this.plusButton
.connect( this, {
12759 click
: [ 'onButtonClick', +1 ]
12761 this.minusButton
.connect( this, {
12762 click
: [ 'onButtonClick', -1 ]
12767 $field
.append( this.$input
);
12768 if ( config
.showButtons
) {
12770 .prepend( this.minusButton
.$element
)
12771 .append( this.plusButton
.$element
);
12775 if ( config
.allowInteger
|| config
.isInteger
) {
12776 // Backward compatibility
12779 this.setRange( config
.min
, config
.max
);
12780 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
12781 // Set the validation method after we set step and range
12782 // so that it doesn't immediately call setValidityFlag
12783 this.setValidation( this.validateNumber
.bind( this ) );
12786 .addClass( 'oo-ui-numberInputWidget' )
12787 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12793 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12797 // Backward compatibility
12798 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12799 this.setStep( flag
? 1 : null );
12801 // Backward compatibility
12802 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12804 // Backward compatibility
12805 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12806 return this.step
=== 1;
12808 // Backward compatibility
12809 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12812 * Set the range of allowed values
12814 * @param {number} min Minimum allowed value
12815 * @param {number} max Maximum allowed value
12817 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12819 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12823 this.$input
.attr( 'min', this.min
);
12824 this.$input
.attr( 'max', this.max
);
12825 this.setValidityFlag();
12829 * Get the current range
12831 * @return {number[]} Minimum and maximum values
12833 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12834 return [ this.min
, this.max
];
12838 * Set the stepping deltas
12840 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12841 * Defaults to `step` if specified, otherwise `1`.
12842 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12843 * Defaults to 10 times `buttonStep`.
12844 * @param {number|null} [step] If specified, the field only accepts values that are multiples
12847 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
12848 if ( buttonStep
=== undefined ) {
12849 buttonStep
= step
|| 1;
12851 if ( pageStep
=== undefined ) {
12852 pageStep
= 10 * buttonStep
;
12854 if ( step
!== null && step
<= 0 ) {
12855 throw new Error( 'Step value, if given, must be positive' );
12857 if ( buttonStep
<= 0 ) {
12858 throw new Error( 'Button step value must be positive' );
12860 if ( pageStep
<= 0 ) {
12861 throw new Error( 'Page step value must be positive' );
12864 this.buttonStep
= buttonStep
;
12865 this.pageStep
= pageStep
;
12866 this.$input
.attr( 'step', this.step
|| 'any' );
12867 this.setValidityFlag();
12873 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12874 if ( value
=== '' ) {
12875 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12876 // so here we make sure an 'empty' value is actually displayed as such.
12877 this.$input
.val( '' );
12879 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12883 * Get the current stepping values
12885 * @return {number[]} Button step, page step, and validity step
12887 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12888 return [ this.buttonStep
, this.pageStep
, this.step
];
12892 * Get the current value of the widget as a number
12894 * @return {number} May be NaN, or an invalid number
12896 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12897 return +this.getValue();
12901 * Adjust the value of the widget
12903 * @param {number} delta Adjustment amount
12905 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12906 var n
, v
= this.getNumericValue();
12909 if ( isNaN( delta
) || !isFinite( delta
) ) {
12910 throw new Error( 'Delta must be a finite number' );
12913 if ( isNaN( v
) ) {
12917 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12919 n
= Math
.round( n
/ this.step
) * this.step
;
12924 this.setValue( n
);
12931 * @param {string} value Field value
12932 * @return {boolean}
12934 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12936 if ( value
=== '' ) {
12937 return !this.isRequired();
12940 if ( isNaN( n
) || !isFinite( n
) ) {
12944 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
12948 if ( n
< this.min
|| n
> this.max
) {
12956 * Handle mouse click events.
12959 * @param {number} dir +1 or -1
12961 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
12962 this.adjustValue( dir
* this.buttonStep
);
12966 * Handle mouse wheel events.
12969 * @param {jQuery.Event} event
12970 * @return {undefined/boolean} False to prevent default if event is handled
12972 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
12975 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
12976 // Standard 'wheel' event
12977 if ( event
.originalEvent
.deltaMode
!== undefined ) {
12978 this.sawWheelEvent
= true;
12980 if ( event
.originalEvent
.deltaY
) {
12981 delta
= -event
.originalEvent
.deltaY
;
12982 } else if ( event
.originalEvent
.deltaX
) {
12983 delta
= event
.originalEvent
.deltaX
;
12986 // Non-standard events
12987 if ( !this.sawWheelEvent
) {
12988 if ( event
.originalEvent
.wheelDeltaX
) {
12989 delta
= -event
.originalEvent
.wheelDeltaX
;
12990 } else if ( event
.originalEvent
.wheelDeltaY
) {
12991 delta
= event
.originalEvent
.wheelDeltaY
;
12992 } else if ( event
.originalEvent
.wheelDelta
) {
12993 delta
= event
.originalEvent
.wheelDelta
;
12994 } else if ( event
.originalEvent
.detail
) {
12995 delta
= -event
.originalEvent
.detail
;
13000 delta
= delta
< 0 ? -1 : 1;
13001 this.adjustValue( delta
* this.buttonStep
);
13009 * Handle key down events.
13012 * @param {jQuery.Event} e Key down event
13013 * @return {undefined/boolean} False to prevent default if event is handled
13015 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
13016 if ( !this.isDisabled() ) {
13017 switch ( e
.which
) {
13018 case OO
.ui
.Keys
.UP
:
13019 this.adjustValue( this.buttonStep
);
13021 case OO
.ui
.Keys
.DOWN
:
13022 this.adjustValue( -this.buttonStep
);
13024 case OO
.ui
.Keys
.PAGEUP
:
13025 this.adjustValue( this.pageStep
);
13027 case OO
.ui
.Keys
.PAGEDOWN
:
13028 this.adjustValue( -this.pageStep
);
13037 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
13039 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13041 if ( this.minusButton
) {
13042 this.minusButton
.setDisabled( this.isDisabled() );
13044 if ( this.plusButton
) {
13045 this.plusButton
.setDisabled( this.isDisabled() );
13052 * SelectFileInputWidgets allow for selecting files, using <input type="file">. These
13053 * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
13054 * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
13055 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
13057 * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename.
13060 * // A file select input widget.
13061 * var selectFile = new OO.ui.SelectFileInputWidget();
13062 * $( document.body ).append( selectFile.$element );
13064 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
13067 * @extends OO.ui.InputWidget
13070 * @param {Object} [config] Configuration options
13071 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
13072 * @cfg {string} [placeholder] Text to display when no file is selected.
13073 * @cfg {Object} [button] Config to pass to select file button.
13074 * @cfg {string} [icon] Icon to show next to file info
13076 OO
.ui
.SelectFileInputWidget
= function OoUiSelectFileInputWidget( config
) {
13077 config
= config
|| {};
13079 // Construct buttons before parent method is called (calling setDisabled)
13080 this.selectButton
= new OO
.ui
.ButtonWidget( $.extend( {
13081 $element
: $( '<label>' ),
13082 classes
: [ 'oo-ui-selectFileInputWidget-selectButton' ],
13083 label
: OO
.ui
.msg( 'ooui-selectfile-button-select' )
13084 }, config
.button
) );
13086 // Configuration initialization
13087 config
= $.extend( {
13089 placeholder
: OO
.ui
.msg( 'ooui-selectfile-placeholder' ),
13090 $tabIndexed
: this.selectButton
.$tabIndexed
13093 this.info
= new OO
.ui
.SearchInputWidget( {
13094 classes
: [ 'oo-ui-selectFileInputWidget-info' ],
13095 placeholder
: config
.placeholder
,
13096 // Pass an empty collection so that .focus() always does nothing
13097 $tabIndexed
: $( [] )
13098 } ).setIcon( config
.icon
);
13099 // Set tabindex manually on $input as $tabIndexed has been overridden
13100 this.info
.$input
.attr( 'tabindex', -1 );
13102 // Parent constructor
13103 OO
.ui
.SelectFileInputWidget
.parent
.call( this, config
);
13106 this.currentFile
= null;
13107 if ( Array
.isArray( config
.accept
) ) {
13108 this.accept
= config
.accept
;
13110 this.accept
= null;
13112 this.onFileSelectedHandler
= this.onFileSelected
.bind( this );
13115 this.info
.connect( this, { change
: 'onInfoChange' } );
13116 this.selectButton
.$button
.on( {
13117 keypress
: this.onKeyPress
.bind( this )
13119 this.connect( this, { change
: 'updateUI' } );
13124 this.fieldLayout
= new OO
.ui
.ActionFieldLayout( this.info
, this.selectButton
, { align
: 'top' } );
13127 .addClass( 'oo-ui-selectFileInputWidget' )
13128 .append( this.fieldLayout
.$element
);
13135 OO
.inheritClass( OO
.ui
.SelectFileInputWidget
, OO
.ui
.InputWidget
);
13140 * Get the filename of the currently selected file.
13142 * @return {string} Filename
13144 OO
.ui
.SelectFileInputWidget
.prototype.getFilename = function () {
13145 if ( this.currentFile
) {
13146 return this.currentFile
.name
;
13148 // Try to strip leading fakepath.
13149 return this.getValue().split( '\\' ).pop();
13156 OO
.ui
.SelectFileInputWidget
.prototype.setValue = function ( value
) {
13157 if ( value
=== undefined ) {
13158 // Called during init, don't replace value if just infusing.
13162 // We need to update this.value, but without trying to modify
13163 // the DOM value, which would throw an exception.
13164 if ( this.value
!== value
) {
13165 this.value
= value
;
13166 this.emit( 'change', this.value
);
13169 this.currentFile
= null;
13171 OO
.ui
.SelectFileInputWidget
.super.prototype.setValue
.call( this, '' );
13176 * Handle file selection from the input.
13179 * @param {jQuery.Event} e
13181 OO
.ui
.SelectFileInputWidget
.prototype.onFileSelected = function ( e
) {
13182 var file
= OO
.getProp( e
.target
, 'files', 0 ) || null;
13184 if ( file
&& !this.isAllowedType( file
.type
) ) {
13188 this.currentFile
= file
;
13192 * Update the user interface when a file is selected or unselected.
13196 OO
.ui
.SelectFileInputWidget
.prototype.updateUI = function () {
13197 this.info
.setValue( this.getFilename() );
13201 * Setup the input element.
13205 OO
.ui
.SelectFileInputWidget
.prototype.setupInput = function () {
13209 // this.selectButton is tabindexed
13211 // Infused input may have previously by
13212 // TabIndexed, so remove aria-disabled attr.
13213 'aria-disabled': null
13215 .on( 'change', this.onFileSelectedHandler
);
13217 if ( this.accept
) {
13218 this.$input
.attr( 'accept', this.accept
.join( ', ' ) );
13220 this.selectButton
.$button
.append( this.$input
);
13224 * Determine if we should accept this file.
13227 * @param {string} mimeType File MIME type
13228 * @return {boolean}
13230 OO
.ui
.SelectFileInputWidget
.prototype.isAllowedType = function ( mimeType
) {
13233 if ( !this.accept
|| !mimeType
) {
13237 for ( i
= 0; i
< this.accept
.length
; i
++ ) {
13238 mimeTest
= this.accept
[ i
];
13239 if ( mimeTest
=== mimeType
) {
13241 } else if ( mimeTest
.substr( -2 ) === '/*' ) {
13242 mimeTest
= mimeTest
.substr( 0, mimeTest
.length
- 1 );
13243 if ( mimeType
.substr( 0, mimeTest
.length
) === mimeTest
) {
13253 * Handle info input change events
13255 * The info widget can only be changed by the user
13256 * with the clear button.
13259 * @param {string} value
13261 OO
.ui
.SelectFileInputWidget
.prototype.onInfoChange = function ( value
) {
13262 if ( value
=== '' ) {
13263 this.setValue( null );
13268 * Handle key press events.
13271 * @param {jQuery.Event} e Key press event
13272 * @return {undefined/boolean} False to prevent default if event is handled
13274 OO
.ui
.SelectFileInputWidget
.prototype.onKeyPress = function ( e
) {
13275 if ( !this.isDisabled() && this.$input
&&
13276 ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
)
13278 // Emit a click to open the file selector.
13279 this.$input
.trigger( 'click' );
13280 // Taking focus from the selectButton means keyUp isn't fired, so fire it manually.
13281 this.selectButton
.onDocumentKeyUp( e
);
13289 OO
.ui
.SelectFileInputWidget
.prototype.setDisabled = function ( disabled
) {
13291 OO
.ui
.SelectFileInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13293 this.selectButton
.setDisabled( disabled
);
13294 this.info
.setDisabled( disabled
);
13301 //# sourceMappingURL=oojs-ui-core.js.map.json