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-07T09:14:18Z
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
= OO
.ui
.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
- ( OO
.ui
.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 * @return {number} Current timestamp, in milliseconds since the Unix epoch
328 OO
.ui
.now
= Date
.now
|| function () {
329 return new Date().getTime();
333 * Reconstitute a JavaScript object corresponding to a widget created by
334 * the PHP implementation.
336 * This is an alias for `OO.ui.Element.static.infuse()`.
338 * @param {string|HTMLElement|jQuery} idOrNode
339 * A DOM id (if a string) or node for the widget to infuse.
340 * @param {Object} [config] Configuration options
341 * @return {OO.ui.Element}
342 * The `OO.ui.Element` corresponding to this (infusable) document node.
344 OO
.ui
.infuse = function ( idOrNode
, config
) {
345 return OO
.ui
.Element
.static.infuse( idOrNode
, config
);
350 * Message store for the default implementation of OO.ui.msg.
352 * Environments that provide a localization system should not use this, but should override
353 * OO.ui.msg altogether.
358 // Tool tip for a button that moves items in a list down one place
359 'ooui-outline-control-move-down': 'Move item down',
360 // Tool tip for a button that moves items in a list up one place
361 'ooui-outline-control-move-up': 'Move item up',
362 // Tool tip for a button that removes items from a list
363 'ooui-outline-control-remove': 'Remove item',
364 // Label for the toolbar group that contains a list of all other available tools
365 'ooui-toolbar-more': 'More',
366 // Label for the fake tool that expands the full list of tools in a toolbar group
367 'ooui-toolgroup-expand': 'More',
368 // Label for the fake tool that collapses the full list of tools in a toolbar group
369 'ooui-toolgroup-collapse': 'Fewer',
370 // Default label for the tooltip for the button that removes a tag item
371 'ooui-item-remove': 'Remove',
372 // Default label for the accept button of a confirmation dialog
373 'ooui-dialog-message-accept': 'OK',
374 // Default label for the reject button of a confirmation dialog
375 'ooui-dialog-message-reject': 'Cancel',
376 // Title for process dialog error description
377 'ooui-dialog-process-error': 'Something went wrong',
378 // Label for process dialog dismiss error button, visible when describing errors
379 'ooui-dialog-process-dismiss': 'Dismiss',
380 // Label for process dialog retry action button, visible when describing only recoverable
382 'ooui-dialog-process-retry': 'Try again',
383 // Label for process dialog retry action button, visible when describing only warnings
384 'ooui-dialog-process-continue': 'Continue',
385 // Label for button in combobox input that triggers its dropdown
386 'ooui-combobox-button-label': 'Dropdown for combobox',
387 // Label for the file selection widget's select file button
388 'ooui-selectfile-button-select': 'Select a file',
389 // Label for the file selection widget if file selection is not supported
390 'ooui-selectfile-not-supported': 'File selection is not supported',
391 // Label for the file selection widget when no file is currently selected
392 'ooui-selectfile-placeholder': 'No file is selected',
393 // Label for the file selection widget's drop target
394 'ooui-selectfile-dragdrop-placeholder': 'Drop file here',
395 // Label for the help icon attached to a form field
396 'ooui-field-help': 'Help'
400 * Get a localized message.
402 * After the message key, message parameters may optionally be passed. In the default
403 * implementation, any occurrences of $1 are replaced with the first parameter, $2 with the
404 * second parameter, etc.
405 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long
406 * as they support unnamed, ordered message parameters.
408 * In environments that provide a localization system, this function should be overridden to
409 * return the message translated in the user's language. The default implementation always
410 * returns English messages. An example of doing this with
411 * [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) follows.
414 * var i, iLen, button,
415 * messagePath = 'oojs-ui/dist/i18n/',
416 * languages = [ $.i18n().locale, 'ur', 'en' ],
419 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
420 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
423 * $.i18n().load( languageMap ).done( function() {
424 * // Replace the built-in `msg` only once we've loaded the internationalization.
425 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
426 * // you put off creating any widgets until this promise is complete, no English
427 * // will be displayed.
428 * OO.ui.msg = $.i18n;
430 * // A button displaying "OK" in the default locale
431 * button = new OO.ui.ButtonWidget( {
432 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
435 * $( document.body ).append( button.$element );
437 * // A button displaying "OK" in Urdu
438 * $.i18n().locale = 'ur';
439 * button = new OO.ui.ButtonWidget( {
440 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
443 * $( document.body ).append( button.$element );
446 * @param {string} key Message key
447 * @param {...Mixed} [params] Message parameters
448 * @return {string} Translated message with parameters substituted
450 OO
.ui
.msg = function ( key
) {
451 var message
= messages
[ key
],
452 params
= Array
.prototype.slice
.call( arguments
, 1 );
453 if ( typeof message
=== 'string' ) {
454 // Perform $1 substitution
455 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
456 var i
= parseInt( n
, 10 );
457 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
460 // Return placeholder if message not found
461 message
= '[' + key
+ ']';
468 * Package a message and arguments for deferred resolution.
470 * Use this when you are statically specifying a message and the message may not yet be present.
472 * @param {string} key Message key
473 * @param {...Mixed} [params] Message parameters
474 * @return {Function} Function that returns the resolved message when executed
476 OO
.ui
.deferMsg = function () {
477 var args
= arguments
;
479 return OO
.ui
.msg
.apply( OO
.ui
, args
);
486 * If the message is a function it will be executed, otherwise it will pass through directly.
488 * @param {Function|string} msg Deferred message, or message text
489 * @return {string} Resolved message
491 OO
.ui
.resolveMsg = function ( msg
) {
492 if ( typeof msg
=== 'function' ) {
499 * @param {string} url
502 OO
.ui
.isSafeUrl = function ( url
) {
503 // Keep this function in sync with php/Tag.php
504 var i
, protocolWhitelist
;
506 function stringStartsWith( haystack
, needle
) {
507 return haystack
.substr( 0, needle
.length
) === needle
;
510 protocolWhitelist
= [
511 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
512 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
513 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
520 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
521 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
526 // This matches '//' too
527 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
530 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
538 * Check if the user has a 'mobile' device.
540 * For our purposes this means the user is primarily using an
541 * on-screen keyboard, touch input instead of a mouse and may
542 * have a physically small display.
544 * It is left up to implementors to decide how to compute this
545 * so the default implementation always returns false.
547 * @return {boolean} User is on a mobile device
549 OO
.ui
.isMobile = function () {
554 * Get the additional spacing that should be taken into account when displaying elements that are
555 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
556 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
558 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
559 * the extra spacing from that edge of viewport (in pixels)
561 OO
.ui
.getViewportSpacing = function () {
571 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
572 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
574 * @return {jQuery} Default overlay node
576 OO
.ui
.getDefaultOverlay = function () {
577 if ( !OO
.ui
.$defaultOverlay
) {
578 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
579 $( document
.body
).append( OO
.ui
.$defaultOverlay
);
581 return OO
.ui
.$defaultOverlay
;
589 * Namespace for OOUI mixins.
591 * Mixins are named according to the type of object they are intended to
592 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
593 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
594 * is intended to be mixed in to an instance of OO.ui.Widget.
602 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
603 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not
604 * have events connected to them and can't be interacted with.
610 * @param {Object} [config] Configuration options
611 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are
612 * added to the top level (e.g., the outermost div) of the element. See the
613 * [OOUI documentation on MediaWiki][2] for an example.
614 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
615 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
616 * @cfg {string} [text] Text to insert
617 * @cfg {Array} [content] An array of content elements to append (after #text).
618 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
619 * Instances of OO.ui.Element will have their $element appended.
620 * @cfg {jQuery} [$content] Content elements to append (after #text).
621 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
622 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number,
624 * Data can also be specified with the #setData method.
626 OO
.ui
.Element
= function OoUiElement( config
) {
627 if ( OO
.ui
.isDemo
) {
628 this.initialConfig
= config
;
630 // Configuration initialization
631 config
= config
|| {};
634 this.$ = function () {
635 OO
.ui
.warnDeprecation( 'this.$ is deprecated, use global $ instead' );
636 return $.apply( this, arguments
);
638 this.elementId
= null;
640 this.data
= config
.data
;
641 this.$element
= config
.$element
||
642 $( document
.createElement( this.getTagName() ) );
643 this.elementGroup
= null;
646 if ( Array
.isArray( config
.classes
) ) {
647 this.$element
.addClass( config
.classes
);
650 this.setElementId( config
.id
);
653 this.$element
.text( config
.text
);
655 if ( config
.content
) {
656 // The `content` property treats plain strings as text; use an
657 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
658 // appropriate $element appended.
659 this.$element
.append( config
.content
.map( function ( v
) {
660 if ( typeof v
=== 'string' ) {
661 // Escape string so it is properly represented in HTML.
662 return document
.createTextNode( v
);
663 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
666 } else if ( v
instanceof OO
.ui
.Element
) {
672 if ( config
.$content
) {
673 // The `$content` property treats plain strings as HTML.
674 this.$element
.append( config
.$content
);
680 OO
.initClass( OO
.ui
.Element
);
682 /* Static Properties */
685 * The name of the HTML tag used by the element.
687 * The static value may be ignored if the #getTagName method is overridden.
693 OO
.ui
.Element
.static.tagName
= 'div';
698 * Reconstitute a JavaScript object corresponding to a widget created
699 * by the PHP implementation.
701 * @param {string|HTMLElement|jQuery} idOrNode
702 * A DOM id (if a string) or node for the widget to infuse.
703 * @param {Object} [config] Configuration options
704 * @return {OO.ui.Element}
705 * The `OO.ui.Element` corresponding to this (infusable) document node.
706 * For `Tag` objects emitted on the HTML side (used occasionally for content)
707 * the value returned is a newly-created Element wrapping around the existing
710 OO
.ui
.Element
.static.infuse = function ( idOrNode
, config
) {
711 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, config
, false );
713 if ( typeof idOrNode
=== 'string' ) {
714 // IDs deprecated since 0.29.7
715 OO
.ui
.warnDeprecation(
716 'Passing a string ID to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
719 // Verify that the type matches up.
720 // FIXME: uncomment after T89721 is fixed, see T90929.
722 if ( !( obj instanceof this['class'] ) ) {
723 throw new Error( 'Infusion type mismatch!' );
730 * Implementation helper for `infuse`; skips the type check and has an
731 * extra property so that only the top-level invocation touches the DOM.
734 * @param {string|HTMLElement|jQuery} idOrNode
735 * @param {Object} [config] Configuration options
736 * @param {jQuery.Promise} [domPromise] A promise that will be resolved
737 * when the top-level widget of this infusion is inserted into DOM,
738 * replacing the original node; only used internally.
739 * @return {OO.ui.Element}
741 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, config
, domPromise
) {
742 // look for a cached result of a previous infusion.
743 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
744 if ( typeof idOrNode
=== 'string' ) {
746 $elem
= $( document
.getElementById( id
) );
748 $elem
= $( idOrNode
);
749 id
= $elem
.attr( 'id' );
751 if ( !$elem
.length
) {
752 if ( typeof idOrNode
=== 'string' ) {
753 error
= 'Widget not found: ' + idOrNode
;
754 } else if ( idOrNode
&& idOrNode
.selector
) {
755 error
= 'Widget not found: ' + idOrNode
.selector
;
757 error
= 'Widget not found';
759 throw new Error( error
);
761 if ( $elem
[ 0 ].oouiInfused
) {
762 $elem
= $elem
[ 0 ].oouiInfused
;
764 data
= $elem
.data( 'ooui-infused' );
767 if ( data
=== true ) {
768 throw new Error( 'Circular dependency! ' + id
);
771 // Pick up dynamic state, like focus, value of form inputs, scroll position, etc.
772 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
773 // Restore dynamic state after the new element is re-inserted into DOM under
775 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
776 infusedChildren
= $elem
.data( 'ooui-infused-children' );
777 if ( infusedChildren
&& infusedChildren
.length
) {
778 infusedChildren
.forEach( function ( data
) {
779 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
780 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
786 data
= $elem
.attr( 'data-ooui' );
788 throw new Error( 'No infusion data found: ' + id
);
791 data
= JSON
.parse( data
);
795 if ( !( data
&& data
._
) ) {
796 throw new Error( 'No valid infusion data found: ' + id
);
798 if ( data
._
=== 'Tag' ) {
799 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
800 return new OO
.ui
.Element( $.extend( {}, config
, { $element
: $elem
} ) );
802 parts
= data
._
.split( '.' );
803 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
804 if ( cls
=== undefined ) {
805 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
808 // Verify that we're creating an OO.ui.Element instance
811 while ( parent
!== undefined ) {
812 if ( parent
=== OO
.ui
.Element
) {
817 parent
= parent
.parent
;
820 if ( parent
!== OO
.ui
.Element
) {
821 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
826 domPromise
= top
.promise();
828 $elem
.data( 'ooui-infused', true ); // prevent loops
829 data
.id
= id
; // implicit
830 infusedChildren
= [];
831 data
= OO
.copy( data
, null, function deserialize( value
) {
833 if ( OO
.isPlainObject( value
) ) {
835 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, config
, domPromise
);
836 infusedChildren
.push( infused
);
837 // Flatten the structure
838 infusedChildren
.push
.apply(
840 infused
.$element
.data( 'ooui-infused-children' ) || []
842 infused
.$element
.removeData( 'ooui-infused-children' );
845 if ( value
.html
!== undefined ) {
846 return new OO
.ui
.HtmlSnippet( value
.html
);
850 // allow widgets to reuse parts of the DOM
851 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
852 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
853 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
855 // eslint-disable-next-line new-cap
856 obj
= new cls( $.extend( {}, config
, data
) );
857 // If anyone is holding a reference to the old DOM element,
858 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
859 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
860 $elem
[ 0 ].oouiInfused
= obj
.$element
;
861 // now replace old DOM with this new DOM.
863 // An efficient constructor might be able to reuse the entire DOM tree of the original
864 // element, so only mutate the DOM if we need to.
865 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
866 $elem
.replaceWith( obj
.$element
);
870 obj
.$element
.data( 'ooui-infused', obj
);
871 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
872 // set the 'data-ooui' attribute so we can identify infused widgets
873 obj
.$element
.attr( 'data-ooui', '' );
874 // restore dynamic state after the new element is inserted into DOM
875 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
880 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
882 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
883 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
884 * constructor, which will be given the enhanced config.
887 * @param {HTMLElement} node
888 * @param {Object} config
891 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
896 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM
897 * node (and its children) that represent an Element of the same class and the given configuration,
898 * generated by the PHP implementation.
900 * This method is called just before `node` is detached from the DOM. The return value of this
901 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
902 * is inserted into DOM to replace `node`.
905 * @param {HTMLElement} node
906 * @param {Object} config
909 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
914 * Get a jQuery function within a specific document.
917 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
918 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
920 * @return {Function} Bound jQuery function
922 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
923 function wrapper( selector
) {
924 return $( selector
, wrapper
.context
);
927 wrapper
.context
= this.getDocument( context
);
930 wrapper
.$iframe
= $iframe
;
937 * Get the document of an element.
940 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
941 * @return {HTMLDocument|null} Document object
943 OO
.ui
.Element
.static.getDocument = function ( obj
) {
944 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
945 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
946 // Empty jQuery selections might have a context
953 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
958 * Get the window of an element or document.
961 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
962 * @return {Window} Window object
964 OO
.ui
.Element
.static.getWindow = function ( obj
) {
965 var doc
= this.getDocument( obj
);
966 return doc
.defaultView
;
970 * Get the direction of an element or document.
973 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
974 * @return {string} Text direction, either 'ltr' or 'rtl'
976 OO
.ui
.Element
.static.getDir = function ( obj
) {
979 if ( obj
instanceof $ ) {
982 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
983 isWin
= obj
.document
!== undefined;
984 if ( isDoc
|| isWin
) {
990 return $( obj
).css( 'direction' );
994 * Get the offset between two frames.
996 * TODO: Make this function not use recursion.
999 * @param {Window} from Window of the child frame
1000 * @param {Window} [to=window] Window of the parent frame
1001 * @param {Object} [offset] Offset to start with, used internally
1002 * @return {Object} Offset object, containing left and top properties
1004 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
1005 var i
, len
, frames
, frame
, rect
;
1011 offset
= { top
: 0, left
: 0 };
1013 if ( from.parent
=== from ) {
1017 // Get iframe element
1018 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
1019 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
1020 if ( frames
[ i
].contentWindow
=== from ) {
1021 frame
= frames
[ i
];
1026 // Recursively accumulate offset values
1028 rect
= frame
.getBoundingClientRect();
1029 offset
.left
+= rect
.left
;
1030 offset
.top
+= rect
.top
;
1031 if ( from !== to
) {
1032 this.getFrameOffset( from.parent
, offset
);
1039 * Get the offset between two elements.
1041 * The two elements may be in a different frame, but in that case the frame $element is in must
1042 * be contained in the frame $anchor is in.
1045 * @param {jQuery} $element Element whose position to get
1046 * @param {jQuery} $anchor Element to get $element's position relative to
1047 * @return {Object} Translated position coordinates, containing top and left properties
1049 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1050 var iframe
, iframePos
,
1051 pos
= $element
.offset(),
1052 anchorPos
= $anchor
.offset(),
1053 elementDocument
= this.getDocument( $element
),
1054 anchorDocument
= this.getDocument( $anchor
);
1056 // If $element isn't in the same document as $anchor, traverse up
1057 while ( elementDocument
!== anchorDocument
) {
1058 iframe
= elementDocument
.defaultView
.frameElement
;
1060 throw new Error( '$element frame is not contained in $anchor frame' );
1062 iframePos
= $( iframe
).offset();
1063 pos
.left
+= iframePos
.left
;
1064 pos
.top
+= iframePos
.top
;
1065 elementDocument
= iframe
.ownerDocument
;
1067 pos
.left
-= anchorPos
.left
;
1068 pos
.top
-= anchorPos
.top
;
1073 * Get element border sizes.
1076 * @param {HTMLElement} el Element to measure
1077 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1079 OO
.ui
.Element
.static.getBorders = function ( el
) {
1080 var doc
= el
.ownerDocument
,
1081 win
= doc
.defaultView
,
1082 style
= win
.getComputedStyle( el
, null ),
1084 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1085 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1086 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1087 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1098 * Get dimensions of an element or window.
1101 * @param {HTMLElement|Window} el Element to measure
1102 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1104 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1106 doc
= el
.ownerDocument
|| el
.document
,
1107 win
= doc
.defaultView
;
1109 if ( win
=== el
|| el
=== doc
.documentElement
) {
1112 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1114 top
: $win
.scrollTop(),
1115 left
: $win
.scrollLeft()
1117 scrollbar
: { right
: 0, bottom
: 0 },
1121 bottom
: $win
.innerHeight(),
1122 right
: $win
.innerWidth()
1128 borders
: this.getBorders( el
),
1130 top
: $el
.scrollTop(),
1131 left
: $el
.scrollLeft()
1134 right
: $el
.innerWidth() - el
.clientWidth
,
1135 bottom
: $el
.innerHeight() - el
.clientHeight
1137 rect
: el
.getBoundingClientRect()
1143 * Get the number of pixels that an element's content is scrolled to the left.
1145 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1146 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1148 * This function smooths out browser inconsistencies (nicely described in the README at
1149 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1150 * with Firefox's 'scrollLeft', which seems the sanest.
1154 * @param {HTMLElement|Window} el Element to measure
1155 * @return {number} Scroll position from the left.
1156 * If the element's direction is LTR, this is a positive number between `0` (initial scroll
1157 * position) and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1158 * If the element's direction is RTL, this is a negative number between `0` (initial scroll
1159 * position) and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1161 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1162 var rtlScrollType
= null;
1165 var $definer
= $( '<div>' ).attr( {
1167 style
: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
1169 definer
= $definer
[ 0 ];
1171 $definer
.appendTo( 'body' );
1172 if ( definer
.scrollLeft
> 0 ) {
1174 rtlScrollType
= 'default';
1176 definer
.scrollLeft
= 1;
1177 if ( definer
.scrollLeft
=== 0 ) {
1178 // Firefox, old Opera
1179 rtlScrollType
= 'negative';
1181 // Internet Explorer, Edge
1182 rtlScrollType
= 'reverse';
1188 return function getScrollLeft( el
) {
1189 var isRoot
= el
.window
=== el
||
1190 el
=== el
.ownerDocument
.body
||
1191 el
=== el
.ownerDocument
.documentElement
,
1192 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1193 // All browsers use the correct scroll type ('negative') on the root, so don't
1194 // do any fixups when looking at the root element
1195 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1197 if ( direction
=== 'rtl' ) {
1198 if ( rtlScrollType
=== null ) {
1201 if ( rtlScrollType
=== 'reverse' ) {
1202 scrollLeft
= -scrollLeft
;
1203 } else if ( rtlScrollType
=== 'default' ) {
1204 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1213 * Get the root scrollable element of given element's document.
1215 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1216 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1217 * lets us use 'body' or 'documentElement' based on what is working.
1219 * https://code.google.com/p/chromium/issues/detail?id=303131
1222 * @param {HTMLElement} el Element to find root scrollable parent for
1223 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1224 * depending on browser
1226 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1227 var scrollTop
, body
;
1229 if ( OO
.ui
.scrollableElement
=== undefined ) {
1230 body
= el
.ownerDocument
.body
;
1231 scrollTop
= body
.scrollTop
;
1234 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1235 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1236 if ( Math
.round( body
.scrollTop
) === 1 ) {
1237 body
.scrollTop
= scrollTop
;
1238 OO
.ui
.scrollableElement
= 'body';
1240 OO
.ui
.scrollableElement
= 'documentElement';
1244 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1248 * Get closest scrollable container.
1250 * Traverses up until either a scrollable element or the root is reached, in which case the root
1251 * scrollable element will be returned (see #getRootScrollableElement).
1254 * @param {HTMLElement} el Element to find scrollable container for
1255 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1256 * @return {HTMLElement} Closest scrollable container
1258 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1260 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1261 // 'overflow-y' have different values, so we need to check the separate properties.
1262 props
= [ 'overflow-x', 'overflow-y' ],
1263 $parent
= $( el
).parent();
1265 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1266 props
= [ 'overflow-' + dimension
];
1269 // Special case for the document root (which doesn't really have any scrollable container,
1270 // since it is the ultimate scrollable container, but this is probably saner than null or
1272 if ( $( el
).is( 'html, body' ) ) {
1273 return this.getRootScrollableElement( el
);
1276 while ( $parent
.length
) {
1277 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1278 return $parent
[ 0 ];
1282 val
= $parent
.css( props
[ i
] );
1283 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will
1284 // never be scrolled in that direction, but they can actually be scrolled
1285 // programatically. The user can unintentionally perform a scroll in such case even if
1286 // the application doesn't scroll programatically, e.g. when jumping to an anchor, or
1287 // when using built-in find functionality.
1288 // This could cause funny issues...
1289 if ( val
=== 'auto' || val
=== 'scroll' ) {
1290 return $parent
[ 0 ];
1293 $parent
= $parent
.parent();
1295 // The element is unattached... return something mostly sane
1296 return this.getRootScrollableElement( el
);
1300 * Scroll element into view.
1303 * @param {HTMLElement} el Element to scroll into view
1304 * @param {Object} [config] Configuration options
1305 * @param {string} [config.duration='fast'] jQuery animation duration value
1306 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1307 * to scroll in both directions
1308 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1310 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1311 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
,
1313 deferred
= $.Deferred();
1315 // Configuration initialization
1316 config
= config
|| {};
1319 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1320 $container
= $( container
);
1321 elementDimensions
= this.getDimensions( el
);
1322 containerDimensions
= this.getDimensions( container
);
1323 $window
= $( this.getWindow( el
) );
1325 // Compute the element's position relative to the container
1326 if ( $container
.is( 'html, body' ) ) {
1327 // If the scrollable container is the root, this is easy
1329 top
: elementDimensions
.rect
.top
,
1330 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1331 left
: elementDimensions
.rect
.left
,
1332 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1335 // Otherwise, we have to subtract el's coordinates from container's coordinates
1337 top
: elementDimensions
.rect
.top
-
1338 ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1339 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
-
1340 containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1341 left
: elementDimensions
.rect
.left
-
1342 ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1343 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
-
1344 containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1348 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1349 if ( position
.top
< 0 ) {
1350 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1351 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1352 animations
.scrollTop
= containerDimensions
.scroll
.top
+
1353 Math
.min( position
.top
, -position
.bottom
);
1356 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1357 if ( position
.left
< 0 ) {
1358 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1359 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1360 animations
.scrollLeft
= containerDimensions
.scroll
.left
+
1361 Math
.min( position
.left
, -position
.right
);
1364 if ( !$.isEmptyObject( animations
) ) {
1365 // eslint-disable-next-line no-jquery/no-animate
1366 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ?
1367 'fast' : config
.duration
);
1368 $container
.queue( function ( next
) {
1375 return deferred
.promise();
1379 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1380 * and reserve space for them, because it probably doesn't.
1382 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1383 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1384 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a
1385 * reflow, and then reattach (or show) them back.
1388 * @param {HTMLElement} el Element to reconsider the scrollbars on
1390 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1391 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1392 // Save scroll position
1393 scrollLeft
= el
.scrollLeft
;
1394 scrollTop
= el
.scrollTop
;
1395 // Detach all children
1396 while ( el
.firstChild
) {
1397 nodes
.push( el
.firstChild
);
1398 el
.removeChild( el
.firstChild
);
1401 // eslint-disable-next-line no-void
1402 void el
.offsetHeight
;
1403 // Reattach all children
1404 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1405 el
.appendChild( nodes
[ i
] );
1407 // Restore scroll position (no-op if scrollbars disappeared)
1408 el
.scrollLeft
= scrollLeft
;
1409 el
.scrollTop
= scrollTop
;
1415 * Toggle visibility of an element.
1417 * @param {boolean} [show] Make element visible, omit to toggle visibility
1420 * @return {OO.ui.Element} The element, for chaining
1422 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1423 show
= show
=== undefined ? !this.visible
: !!show
;
1425 if ( show
!== this.isVisible() ) {
1426 this.visible
= show
;
1427 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1428 this.emit( 'toggle', show
);
1435 * Check if element is visible.
1437 * @return {boolean} element is visible
1439 OO
.ui
.Element
.prototype.isVisible = function () {
1440 return this.visible
;
1446 * @return {Mixed} Element data
1448 OO
.ui
.Element
.prototype.getData = function () {
1455 * @param {Mixed} data Element data
1457 * @return {OO.ui.Element} The element, for chaining
1459 OO
.ui
.Element
.prototype.setData = function ( data
) {
1465 * Set the element has an 'id' attribute.
1467 * @param {string} id
1469 * @return {OO.ui.Element} The element, for chaining
1471 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1472 this.elementId
= id
;
1473 this.$element
.attr( 'id', id
);
1478 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1479 * and return its value.
1483 OO
.ui
.Element
.prototype.getElementId = function () {
1484 if ( this.elementId
=== null ) {
1485 this.setElementId( OO
.ui
.generateElementId() );
1487 return this.elementId
;
1491 * Check if element supports one or more methods.
1493 * @param {string|string[]} methods Method or list of methods to check
1494 * @return {boolean} All methods are supported
1496 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1500 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1501 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1502 if ( typeof this[ methods
[ i
] ] === 'function' ) {
1507 return methods
.length
=== support
;
1511 * Update the theme-provided classes.
1513 * @localdoc This is called in element mixins and widget classes any time state changes.
1514 * Updating is debounced, minimizing overhead of changing multiple attributes and
1515 * guaranteeing that theme updates do not occur within an element's constructor
1517 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1518 OO
.ui
.theme
.queueUpdateElementClasses( this );
1522 * Get the HTML tag name.
1524 * Override this method to base the result on instance information.
1526 * @return {string} HTML tag name
1528 OO
.ui
.Element
.prototype.getTagName = function () {
1529 return this.constructor.static.tagName
;
1533 * Check if the element is attached to the DOM
1535 * @return {boolean} The element is attached to the DOM
1537 OO
.ui
.Element
.prototype.isElementAttached = function () {
1538 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1542 * Get the DOM document.
1544 * @return {HTMLDocument} Document object
1546 OO
.ui
.Element
.prototype.getElementDocument = function () {
1547 // Don't cache this in other ways either because subclasses could can change this.$element
1548 return OO
.ui
.Element
.static.getDocument( this.$element
);
1552 * Get the DOM window.
1554 * @return {Window} Window object
1556 OO
.ui
.Element
.prototype.getElementWindow = function () {
1557 return OO
.ui
.Element
.static.getWindow( this.$element
);
1561 * Get closest scrollable container.
1563 * @return {HTMLElement} Closest scrollable container
1565 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1566 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1570 * Get group element is in.
1572 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1574 OO
.ui
.Element
.prototype.getElementGroup = function () {
1575 return this.elementGroup
;
1579 * Set group element is in.
1581 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1583 * @return {OO.ui.Element} The element, for chaining
1585 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1586 this.elementGroup
= group
;
1591 * Scroll element into view.
1593 * @param {Object} [config] Configuration options
1594 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1596 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1598 !this.isElementAttached() ||
1599 !this.isVisible() ||
1600 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1602 return $.Deferred().resolve();
1604 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1608 * Restore the pre-infusion dynamic state for this widget.
1610 * This method is called after #$element has been inserted into DOM. The parameter is the return
1611 * value of #gatherPreInfuseState.
1614 * @param {Object} state
1616 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1620 * Wraps an HTML snippet for use with configuration values which default
1621 * to strings. This bypasses the default html-escaping done to string
1627 * @param {string} [content] HTML content
1629 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1631 this.content
= content
;
1636 OO
.initClass( OO
.ui
.HtmlSnippet
);
1643 * @return {string} Unchanged HTML snippet.
1645 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1646 return this.content
;
1650 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in
1651 * a way that is centrally controlled and can be updated dynamically. Layouts can be, and usually
1653 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout},
1654 * {@link OO.ui.FormLayout FormLayout}, {@link OO.ui.PanelLayout PanelLayout},
1655 * {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1656 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout}
1657 * for more information and examples.
1661 * @extends OO.ui.Element
1662 * @mixins OO.EventEmitter
1665 * @param {Object} [config] Configuration options
1667 OO
.ui
.Layout
= function OoUiLayout( config
) {
1668 // Configuration initialization
1669 config
= config
|| {};
1671 // Parent constructor
1672 OO
.ui
.Layout
.parent
.call( this, config
);
1674 // Mixin constructors
1675 OO
.EventEmitter
.call( this );
1678 this.$element
.addClass( 'oo-ui-layout' );
1683 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1684 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1689 * Reset scroll offsets
1692 * @return {OO.ui.Layout} The layout, for chaining
1694 OO
.ui
.Layout
.prototype.resetScroll = function () {
1695 this.$element
[ 0 ].scrollTop
= 0;
1696 // TODO: Reset scrollLeft in an RTL-aware manner, see OO.ui.Element.static.getScrollLeft.
1702 * Widgets are compositions of one or more OOUI elements that users can both view
1703 * and interact with. All widgets can be configured and modified via a standard API,
1704 * and their state can change dynamically according to a model.
1708 * @extends OO.ui.Element
1709 * @mixins OO.EventEmitter
1712 * @param {Object} [config] Configuration options
1713 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1714 * appearance reflects this state.
1716 OO
.ui
.Widget
= function OoUiWidget( config
) {
1717 // Initialize config
1718 config
= $.extend( { disabled
: false }, config
);
1720 // Parent constructor
1721 OO
.ui
.Widget
.parent
.call( this, config
);
1723 // Mixin constructors
1724 OO
.EventEmitter
.call( this );
1727 this.disabled
= null;
1728 this.wasDisabled
= null;
1731 this.$element
.addClass( 'oo-ui-widget' );
1732 this.setDisabled( !!config
.disabled
);
1737 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1738 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1745 * A 'disable' event is emitted when the disabled state of the widget changes
1746 * (i.e. on disable **and** enable).
1748 * @param {boolean} disabled Widget is disabled
1754 * A 'toggle' event is emitted when the visibility of the widget changes.
1756 * @param {boolean} visible Widget is visible
1762 * Check if the widget is disabled.
1764 * @return {boolean} Widget is disabled
1766 OO
.ui
.Widget
.prototype.isDisabled = function () {
1767 return this.disabled
;
1771 * Set the 'disabled' state of the widget.
1773 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1775 * @param {boolean} disabled Disable widget
1777 * @return {OO.ui.Widget} The widget, for chaining
1779 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1782 this.disabled
= !!disabled
;
1783 isDisabled
= this.isDisabled();
1784 if ( isDisabled
!== this.wasDisabled
) {
1785 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1786 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1787 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1788 this.emit( 'disable', isDisabled
);
1789 this.updateThemeClasses();
1791 this.wasDisabled
= isDisabled
;
1797 * Update the disabled state, in case of changes in parent widget.
1800 * @return {OO.ui.Widget} The widget, for chaining
1802 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1803 this.setDisabled( this.disabled
);
1808 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1811 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1814 * @return {string|null} The ID of the labelable element
1816 OO
.ui
.Widget
.prototype.getInputId = function () {
1821 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1822 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1823 * override this method to provide intuitive, accessible behavior.
1825 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1826 * Individual widgets may override it too.
1828 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1831 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1842 OO
.ui
.Theme
= function OoUiTheme() {
1843 this.elementClassesQueue
= [];
1844 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1849 OO
.initClass( OO
.ui
.Theme
);
1854 * Get a list of classes to be applied to a widget.
1856 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1857 * otherwise state transitions will not work properly.
1859 * @param {OO.ui.Element} element Element for which to get classes
1860 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1862 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1863 return { on
: [], off
: [] };
1867 * Update CSS classes provided by the theme.
1869 * For elements with theme logic hooks, this should be called any time there's a state change.
1871 * @param {OO.ui.Element} element Element for which to update classes
1873 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1874 var $elements
= $( [] ),
1875 classes
= this.getElementClasses( element
);
1877 if ( element
.$icon
) {
1878 $elements
= $elements
.add( element
.$icon
);
1880 if ( element
.$indicator
) {
1881 $elements
= $elements
.add( element
.$indicator
);
1885 .removeClass( classes
.off
)
1886 .addClass( classes
.on
);
1892 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1894 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1895 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1898 this.elementClassesQueue
= [];
1902 * Queue #updateElementClasses to be called for this element.
1904 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1905 * to make them synchronous.
1907 * @param {OO.ui.Element} element Element for which to update classes
1909 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1910 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1911 // the most common case (this method is often called repeatedly for the same element).
1912 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1915 this.elementClassesQueue
.push( element
);
1916 this.debouncedUpdateQueuedElementClasses();
1920 * Get the transition duration in milliseconds for dialogs opening/closing
1922 * The dialog should be fully rendered this many milliseconds after the
1923 * ready process has executed.
1925 * @return {number} Transition duration in milliseconds
1927 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1932 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1933 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1934 * order in which users will navigate through the focusable elements via the Tab key.
1937 * // TabIndexedElement is mixed into the ButtonWidget class
1938 * // to provide a tabIndex property.
1939 * var button1 = new OO.ui.ButtonWidget( {
1943 * button2 = new OO.ui.ButtonWidget( {
1947 * button3 = new OO.ui.ButtonWidget( {
1951 * button4 = new OO.ui.ButtonWidget( {
1955 * $( document.body ).append(
1966 * @param {Object} [config] Configuration options
1967 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1968 * the functionality is applied to the element created by the class ($element). If a different
1969 * element is specified, the tabindex functionality will be applied to it instead.
1970 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the
1971 * tab-navigation order (e.g., 1 for the first focusable element). Use 0 to use the default
1972 * navigation order; use -1 to remove the element from the tab-navigation flow.
1974 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1975 // Configuration initialization
1976 config
= $.extend( { tabIndex
: 0 }, config
);
1979 this.$tabIndexed
= null;
1980 this.tabIndex
= null;
1983 this.connect( this, {
1984 disable
: 'onTabIndexedElementDisable'
1988 this.setTabIndex( config
.tabIndex
);
1989 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1994 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1999 * Set the element that should use the tabindex functionality.
2001 * This method is used to retarget a tabindex mixin so that its functionality applies
2002 * to the specified element. If an element is currently using the functionality, the mixin’s
2003 * effect on that element is removed before the new element is set up.
2005 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
2007 * @return {OO.ui.Element} The element, for chaining
2009 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
2010 var tabIndex
= this.tabIndex
;
2011 // Remove attributes from old $tabIndexed
2012 this.setTabIndex( null );
2013 // Force update of new $tabIndexed
2014 this.$tabIndexed
= $tabIndexed
;
2015 this.tabIndex
= tabIndex
;
2016 return this.updateTabIndex();
2020 * Set the value of the tabindex.
2022 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
2024 * @return {OO.ui.Element} The element, for chaining
2026 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
2027 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
2029 if ( this.tabIndex
!== tabIndex
) {
2030 this.tabIndex
= tabIndex
;
2031 this.updateTabIndex();
2038 * Update the `tabindex` attribute, in case of changes to tab index or
2043 * @return {OO.ui.Element} The element, for chaining
2045 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
2046 if ( this.$tabIndexed
) {
2047 if ( this.tabIndex
!== null ) {
2048 // Do not index over disabled elements
2049 this.$tabIndexed
.attr( {
2050 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
2051 // Support: ChromeVox and NVDA
2052 // These do not seem to inherit aria-disabled from parent elements
2053 'aria-disabled': this.isDisabled().toString()
2056 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
2063 * Handle disable events.
2066 * @param {boolean} disabled Element is disabled
2068 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
2069 this.updateTabIndex();
2073 * Get the value of the tabindex.
2075 * @return {number|null} Tabindex value
2077 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2078 return this.tabIndex
;
2082 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2084 * If the element already has an ID then that is returned, otherwise unique ID is
2085 * generated, set on the element, and returned.
2087 * @return {string|null} The ID of the focusable element
2089 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2092 if ( !this.$tabIndexed
) {
2095 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2099 id
= this.$tabIndexed
.attr( 'id' );
2100 if ( id
=== undefined ) {
2101 id
= OO
.ui
.generateElementId();
2102 this.$tabIndexed
.attr( 'id', id
);
2109 * Whether the node is 'labelable' according to the HTML spec
2110 * (i.e., whether it can be interacted with through a `<label for="…">`).
2111 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2114 * @param {jQuery} $node
2117 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2119 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2120 tagName
= $node
.prop( 'tagName' ).toLowerCase();
2122 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2125 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2132 * Focus this element.
2135 * @return {OO.ui.Element} The element, for chaining
2137 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2138 if ( !this.isDisabled() ) {
2139 this.$tabIndexed
.trigger( 'focus' );
2145 * Blur this element.
2148 * @return {OO.ui.Element} The element, for chaining
2150 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2151 this.$tabIndexed
.trigger( 'blur' );
2156 * @inheritdoc OO.ui.Widget
2158 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2163 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2164 * interface element that can be configured with access keys for keyboard interaction.
2165 * See the [OOUI documentation on MediaWiki] [1] for examples.
2167 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2173 * @param {Object} [config] Configuration options
2174 * @cfg {jQuery} [$button] The button element created by the class.
2175 * If this configuration is omitted, the button element will use a generated `<a>`.
2176 * @cfg {boolean} [framed=true] Render the button with a frame
2178 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2179 // Configuration initialization
2180 config
= config
|| {};
2183 this.$button
= null;
2185 this.active
= config
.active
!== undefined && config
.active
;
2186 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
2187 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2188 this.onDocumentKeyUpHandler
= this.onDocumentKeyUp
.bind( this );
2189 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2190 this.onClickHandler
= this.onClick
.bind( this );
2191 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2194 this.$element
.addClass( 'oo-ui-buttonElement' );
2195 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2196 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2201 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2203 /* Static Properties */
2206 * Cancel mouse down events.
2208 * This property is usually set to `true` to prevent the focus from changing when the button is
2210 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and
2211 * {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} use a value of `false` so that dragging
2212 * behavior is possible and mousedown events can be handled by a parent widget.
2216 * @property {boolean}
2218 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2223 * A 'click' event is emitted when the button element is clicked.
2231 * Set the button element.
2233 * This method is used to retarget a button mixin so that its functionality applies to
2234 * the specified button element instead of the one created by the class. If a button element
2235 * is already set, the method will remove the mixin’s effect on that element.
2237 * @param {jQuery} $button Element to use as button
2239 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2240 if ( this.$button
) {
2242 .removeClass( 'oo-ui-buttonElement-button' )
2243 .removeAttr( 'role accesskey' )
2245 mousedown
: this.onMouseDownHandler
,
2246 keydown
: this.onKeyDownHandler
,
2247 click
: this.onClickHandler
,
2248 keypress
: this.onKeyPressHandler
2252 this.$button
= $button
2253 .addClass( 'oo-ui-buttonElement-button' )
2255 mousedown
: this.onMouseDownHandler
,
2256 keydown
: this.onKeyDownHandler
,
2257 click
: this.onClickHandler
,
2258 keypress
: this.onKeyPressHandler
2261 // Add `role="button"` on `<a>` elements, where it's needed
2262 // `toUpperCase()` is added for XHTML documents
2263 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2264 this.$button
.attr( 'role', 'button' );
2269 * Handles mouse down events.
2272 * @param {jQuery.Event} e Mouse down event
2273 * @return {undefined/boolean} False to prevent default if event is handled
2275 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2276 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2279 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2280 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2281 // reliably remove the pressed class
2282 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2283 // Prevent change of focus unless specifically configured otherwise
2284 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2290 * Handles document mouse up events.
2293 * @param {MouseEvent} e Mouse up event
2295 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentMouseUp = function ( e
) {
2296 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2299 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2300 // Stop listening for mouseup, since we only needed this once
2301 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2304 // Deprecated alias since 0.28.3
2305 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function () {
2306 OO
.ui
.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
2307 this.onDocumentMouseUp
.apply( this, arguments
);
2311 * Handles mouse click events.
2314 * @param {jQuery.Event} e Mouse click event
2316 * @return {undefined/boolean} False to prevent default if event is handled
2318 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2319 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2320 if ( this.emit( 'click' ) ) {
2327 * Handles key down events.
2330 * @param {jQuery.Event} e Key down event
2332 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2333 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2336 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2337 // Run the keyup handler no matter where the key is when the button is let go, so we can
2338 // reliably remove the pressed class
2339 this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2343 * Handles document key up events.
2346 * @param {KeyboardEvent} e Key up event
2348 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentKeyUp = function ( e
) {
2349 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2352 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2353 // Stop listening for keyup, since we only needed this once
2354 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2357 // Deprecated alias since 0.28.3
2358 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function () {
2359 OO
.ui
.warnDeprecation( 'onKeyUp is deprecated, use onDocumentKeyUp instead' );
2360 this.onDocumentKeyUp
.apply( this, arguments
);
2364 * Handles key press events.
2367 * @param {jQuery.Event} e Key press event
2369 * @return {undefined/boolean} False to prevent default if event is handled
2371 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2372 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2373 if ( this.emit( 'click' ) ) {
2380 * Check if button has a frame.
2382 * @return {boolean} Button is framed
2384 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2389 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame
2392 * @param {boolean} [framed] Make button framed, omit to toggle
2394 * @return {OO.ui.Element} The element, for chaining
2396 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2397 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2398 if ( framed
!== this.framed
) {
2399 this.framed
= framed
;
2401 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2402 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2403 this.updateThemeClasses();
2410 * Set the button's active state.
2412 * The active state can be set on:
2414 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2415 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2416 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2419 * @param {boolean} value Make button active
2421 * @return {OO.ui.Element} The element, for chaining
2423 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2424 this.active
= !!value
;
2425 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2426 this.updateThemeClasses();
2431 * Check if the button is active
2434 * @return {boolean} The button is active
2436 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2441 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2442 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2443 * items from the group is done through the interface the class provides.
2444 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2446 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2449 * @mixins OO.EmitterList
2453 * @param {Object} [config] Configuration options
2454 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2455 * is omitted, the group element will use a generated `<div>`.
2457 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2458 // Configuration initialization
2459 config
= config
|| {};
2461 // Mixin constructors
2462 OO
.EmitterList
.call( this, config
);
2468 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2473 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2480 * A change event is emitted when the set of selected items changes.
2482 * @param {OO.ui.Element[]} items Items currently in the group
2488 * Set the group element.
2490 * If an element is already set, items will be moved to the new element.
2492 * @param {jQuery} $group Element to use as group
2494 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2497 this.$group
= $group
;
2498 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2499 this.$group
.append( this.items
[ i
].$element
);
2504 * Find an item by its data.
2506 * Only the first item with matching data will be returned. To return all matching items,
2507 * use the #findItemsFromData method.
2509 * @param {Object} data Item data to search for
2510 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2512 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2514 hash
= OO
.getHash( data
);
2516 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2517 item
= this.items
[ i
];
2518 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2527 * Find items by their data.
2529 * All items with matching data will be returned. To return only the first match, use the
2530 * #findItemFromData method instead.
2532 * @param {Object} data Item data to search for
2533 * @return {OO.ui.Element[]} Items with equivalent data
2535 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2537 hash
= OO
.getHash( data
),
2540 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2541 item
= this.items
[ i
];
2542 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2551 * Add items to the group.
2553 * Items will be added to the end of the group array unless the optional `index` parameter
2554 * specifies a different insertion point. Adding an existing item will move it to the end of the
2555 * array or the point specified by the `index`.
2557 * @param {OO.ui.Element[]} items An array of items to add to the group
2558 * @param {number} [index] Index of the insertion point
2560 * @return {OO.ui.Element} The element, for chaining
2562 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2564 if ( items
.length
=== 0 ) {
2569 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2571 this.emit( 'change', this.getItems() );
2578 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2579 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2580 this.insertItemElements( items
, newIndex
);
2583 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2591 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2592 item
.setElementGroup( this );
2593 this.insertItemElements( item
, index
);
2596 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2602 * Insert elements into the group
2605 * @param {OO.ui.Element} itemWidget Item to insert
2606 * @param {number} index Insertion index
2608 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2609 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2610 this.$group
.append( itemWidget
.$element
);
2611 } else if ( index
=== 0 ) {
2612 this.$group
.prepend( itemWidget
.$element
);
2614 this.items
[ index
].$element
.before( itemWidget
.$element
);
2619 * Remove the specified items from a group.
2621 * Removed items are detached (not removed) from the DOM so that they may be reused.
2622 * To remove all items from a group, you may wish to use the #clearItems method instead.
2624 * @param {OO.ui.Element[]} items An array of items to remove
2626 * @return {OO.ui.Element} The element, for chaining
2628 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2629 var i
, len
, item
, index
;
2631 if ( items
.length
=== 0 ) {
2635 // Remove specific items elements
2636 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2638 index
= this.items
.indexOf( item
);
2639 if ( index
!== -1 ) {
2640 item
.setElementGroup( null );
2641 item
.$element
.detach();
2646 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2648 this.emit( 'change', this.getItems() );
2653 * Clear all items from the group.
2655 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2656 * To remove only a subset of items from a group, use the #removeItems method.
2659 * @return {OO.ui.Element} The element, for chaining
2661 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2664 // Remove all item elements
2665 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2666 this.items
[ i
].setElementGroup( null );
2667 this.items
[ i
].$element
.detach();
2671 OO
.EmitterList
.prototype.clearItems
.call( this );
2673 this.emit( 'change', this.getItems() );
2678 * LabelElement is often mixed into other classes to generate a label, which
2679 * helps identify the function of an interface element.
2680 * See the [OOUI documentation on MediaWiki] [1] for more information.
2682 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2688 * @param {Object} [config] Configuration options
2689 * @cfg {jQuery} [$label] The label element created by the class. If this
2690 * configuration is omitted, the label element will use a generated `<span>`.
2691 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be
2692 * specified as a plaintext string, a jQuery selection of elements, or a function that will
2693 * produce a string in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2694 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2695 * @cfg {boolean} [invisibleLabel] Whether the label should be visually hidden (but still
2696 * accessible to screen-readers).
2698 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2699 // Configuration initialization
2700 config
= config
|| {};
2705 this.invisibleLabel
= null;
2708 this.setLabel( config
.label
|| this.constructor.static.label
);
2709 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2710 this.setInvisibleLabel( config
.invisibleLabel
);
2715 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2720 * @event labelChange
2721 * @param {string} value
2724 /* Static Properties */
2727 * The label text. The label can be specified as a plaintext string, a function that will
2728 * produce a string in the future, or `null` for no label. The static value will
2729 * be overridden if a label is specified with the #label config option.
2733 * @property {string|Function|null}
2735 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2737 /* Static methods */
2740 * Highlight the first occurrence of the query in the given text
2742 * @param {string} text Text
2743 * @param {string} query Query to find
2744 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2745 * @return {jQuery} Text with the first match of the query
2746 * sub-string wrapped in highlighted span
2748 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2751 $result
= $( '<span>' );
2755 qLen
= query
.length
;
2756 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2757 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2762 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2765 if ( !query
.length
|| offset
=== -1 ) {
2766 $result
.text( text
);
2769 document
.createTextNode( text
.slice( 0, offset
) ),
2771 .addClass( 'oo-ui-labelElement-label-highlight' )
2772 .text( text
.slice( offset
, offset
+ query
.length
) ),
2773 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2776 return $result
.contents();
2782 * Set the label element.
2784 * If an element is already set, it will be cleaned up before setting up the new element.
2786 * @param {jQuery} $label Element to use as label
2788 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2789 if ( this.$label
) {
2790 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2793 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2794 this.setLabelContent( this.label
);
2800 * An empty string will result in the label being hidden. A string containing only whitespace will
2801 * be converted to a single ` `.
2803 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that
2804 * returns nodes or text; or null for no label
2806 * @return {OO.ui.Element} The element, for chaining
2808 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2809 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2810 label
= ( ( typeof label
=== 'string' || label
instanceof $ ) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2812 if ( this.label
!== label
) {
2813 if ( this.$label
) {
2814 this.setLabelContent( label
);
2817 this.emit( 'labelChange' );
2820 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2826 * Set whether the label should be visually hidden (but still accessible to screen-readers).
2828 * @param {boolean} invisibleLabel
2830 * @return {OO.ui.Element} The element, for chaining
2832 OO
.ui
.mixin
.LabelElement
.prototype.setInvisibleLabel = function ( invisibleLabel
) {
2833 invisibleLabel
= !!invisibleLabel
;
2835 if ( this.invisibleLabel
!== invisibleLabel
) {
2836 this.invisibleLabel
= invisibleLabel
;
2837 this.emit( 'labelChange' );
2840 this.$label
.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel
);
2841 // Pretend that there is no label, a lot of CSS has been written with this assumption
2842 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2848 * Set the label as plain text with a highlighted query
2850 * @param {string} text Text label to set
2851 * @param {string} query Substring of text to highlight
2852 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2854 * @return {OO.ui.Element} The element, for chaining
2856 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
2857 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
2863 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2864 * text; or null for no label
2866 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2871 * Set the content of the label.
2873 * Do not call this method until after the label element has been set by #setLabelElement.
2876 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2877 * text; or null for no label
2879 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2880 if ( typeof label
=== 'string' ) {
2881 if ( label
.match( /^\s*$/ ) ) {
2882 // Convert whitespace only string to a single non-breaking space
2883 this.$label
.html( ' ' );
2885 this.$label
.text( label
);
2887 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2888 this.$label
.html( label
.toString() );
2889 } else if ( label
instanceof $ ) {
2890 this.$label
.empty().append( label
);
2892 this.$label
.empty();
2897 * IconElement is often mixed into other classes to generate an icon.
2898 * Icons are graphics, about the size of normal text. They are used to aid the user
2899 * in locating a control or to convey information in a space-efficient way. See the
2900 * [OOUI documentation on MediaWiki] [1] for a list of icons
2901 * included in the library.
2903 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2909 * @param {Object} [config] Configuration options
2910 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2911 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2912 * the icon element be set to an existing icon instead of the one generated by this class, set a
2913 * value using a jQuery selection. For example:
2915 * // Use a <div> tag instead of a <span>
2916 * $icon: $( '<div>' )
2917 * // Use an existing icon element instead of the one generated by the class
2918 * $icon: this.$element
2919 * // Use an icon element from a child widget
2920 * $icon: this.childwidget.$element
2921 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a
2922 * map of symbolic names. A map is used for i18n purposes and contains a `default` icon
2923 * name and additional names keyed by language code. The `default` name is used when no icon is
2924 * keyed by the user's language.
2926 * Example of an i18n map:
2928 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2929 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2930 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2931 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that
2932 * returns title text. The icon title is displayed when users move the mouse over the icon.
2934 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2935 // Configuration initialization
2936 config
= config
|| {};
2941 this.iconTitle
= null;
2943 // `iconTitle`s are deprecated since 0.30.0
2944 if ( config
.iconTitle
!== undefined ) {
2945 OO
.ui
.warnDeprecation( 'IconElement: Widgets with iconTitle set are deprecated, use title instead. See T76638 for details.' );
2949 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2950 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2951 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2956 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2958 /* Static Properties */
2961 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map
2962 * is used for i18n purposes and contains a `default` icon name and additional names keyed by
2963 * language code. The `default` name is used when no icon is keyed by the user's language.
2965 * Example of an i18n map:
2967 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2969 * Note: the static property will be overridden if the #icon configuration is used.
2973 * @property {Object|string}
2975 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2978 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2979 * function that returns title text, or `null` for no title.
2981 * The static property will be overridden if the #iconTitle configuration is used.
2985 * @property {string|Function|null}
2987 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2992 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2993 * applies to the specified icon element instead of the one created by the class. If an icon
2994 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2995 * and mixin methods will no longer affect the element.
2997 * @param {jQuery} $icon Element to use as icon
2999 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
3002 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
3003 .removeAttr( 'title' );
3007 .addClass( 'oo-ui-iconElement-icon' )
3008 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
3009 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
3010 if ( this.iconTitle
!== null ) {
3011 this.$icon
.attr( 'title', this.iconTitle
);
3014 this.updateThemeClasses();
3018 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
3019 * The icon parameter can also be set to a map of icon names. See the #icon config setting
3022 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
3023 * by language code, or `null` to remove the icon.
3025 * @return {OO.ui.Element} The element, for chaining
3027 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
3028 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
3029 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
3031 if ( this.icon
!== icon
) {
3033 if ( this.icon
!== null ) {
3034 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
3036 if ( icon
!== null ) {
3037 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
3043 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
3045 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
3047 this.updateThemeClasses();
3053 * Set the icon title. Use `null` to remove the title.
3055 * @param {string|Function|null} iconTitle A text string used as the icon title,
3056 * a function that returns title text, or `null` for no title.
3058 * @return {OO.ui.Element} The element, for chaining
3061 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
3063 ( typeof iconTitle
=== 'function' || ( typeof iconTitle
=== 'string' && iconTitle
.length
) ) ?
3064 OO
.ui
.resolveMsg( iconTitle
) : null;
3066 if ( this.iconTitle
!== iconTitle
) {
3067 this.iconTitle
= iconTitle
;
3069 if ( this.iconTitle
!== null ) {
3070 this.$icon
.attr( 'title', iconTitle
);
3072 this.$icon
.removeAttr( 'title' );
3077 // `setIconTitle` is deprecated since 0.30.0
3078 if ( iconTitle
!== null ) {
3079 // Avoid a warning when this is called from the constructor with no iconTitle set
3080 OO
.ui
.warnDeprecation( 'IconElement: setIconTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
3087 * Get the symbolic name of the icon.
3089 * @return {string} Icon name
3091 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
3096 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
3098 * @return {string} Icon title text
3101 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
3102 return this.iconTitle
;
3106 * IndicatorElement is often mixed into other classes to generate an indicator.
3107 * Indicators are small graphics that are generally used in two ways:
3109 * - To draw attention to the status of an item. For example, an indicator might be
3110 * used to show that an item in a list has errors that need to be resolved.
3111 * - To clarify the function of a control that acts in an exceptional way (a button
3112 * that opens a menu instead of performing an action directly, for example).
3114 * For a list of indicators included in the library, please see the
3115 * [OOUI documentation on MediaWiki] [1].
3117 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3123 * @param {Object} [config] Configuration options
3124 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
3125 * configuration is omitted, the indicator element will use a generated `<span>`.
3126 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3127 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3129 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3130 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
3131 * or a function that returns title text. The indicator title is displayed when users move
3132 * the mouse over the indicator.
3134 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
3135 // Configuration initialization
3136 config
= config
|| {};
3139 this.$indicator
= null;
3140 this.indicator
= null;
3141 this.indicatorTitle
= null;
3143 // `indicatorTitle`s are deprecated since 0.30.0
3144 if ( config
.indicatorTitle
!== undefined ) {
3145 OO
.ui
.warnDeprecation( 'IndicatorElement: Widgets with indicatorTitle set are deprecated, use title instead. See T76638 for details.' );
3149 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
3150 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
3151 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
3156 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
3158 /* Static Properties */
3161 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3162 * The static property will be overridden if the #indicator configuration is used.
3166 * @property {string|null}
3168 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
3171 * A text string used as the indicator title, a function that returns title text, or `null`
3172 * for no title. The static property will be overridden if the #indicatorTitle configuration is
3177 * @property {string|Function|null}
3179 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
3184 * Set the indicator element.
3186 * If an element is already set, it will be cleaned up before setting up the new element.
3188 * @param {jQuery} $indicator Element to use as indicator
3190 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
3191 if ( this.$indicator
) {
3193 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
3194 .removeAttr( 'title' );
3197 this.$indicator
= $indicator
3198 .addClass( 'oo-ui-indicatorElement-indicator' )
3199 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
3200 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
3201 if ( this.indicatorTitle
!== null ) {
3202 this.$indicator
.attr( 'title', this.indicatorTitle
);
3205 this.updateThemeClasses();
3209 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null`
3210 * to remove the indicator.
3212 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3214 * @return {OO.ui.Element} The element, for chaining
3216 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
3217 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
3219 if ( this.indicator
!== indicator
) {
3220 if ( this.$indicator
) {
3221 if ( this.indicator
!== null ) {
3222 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
3224 if ( indicator
!== null ) {
3225 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
3228 this.indicator
= indicator
;
3231 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
3232 if ( this.$indicator
) {
3233 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
3235 this.updateThemeClasses();
3241 * Set the indicator title.
3243 * The title is displayed when a user moves the mouse over the indicator.
3245 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text,
3246 * or `null` for no indicator title
3248 * @return {OO.ui.Element} The element, for chaining
3251 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
3253 ( typeof indicatorTitle
=== 'function' || ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ) ?
3254 OO
.ui
.resolveMsg( indicatorTitle
) : null;
3256 if ( this.indicatorTitle
!== indicatorTitle
) {
3257 this.indicatorTitle
= indicatorTitle
;
3258 if ( this.$indicator
) {
3259 if ( this.indicatorTitle
!== null ) {
3260 this.$indicator
.attr( 'title', indicatorTitle
);
3262 this.$indicator
.removeAttr( 'title' );
3267 // `setIndicatorTitle` is deprecated since 0.30.0
3268 if ( indicatorTitle
!== null ) {
3269 // Avoid a warning when this is called from the constructor with no indicatorTitle set
3270 OO
.ui
.warnDeprecation( 'IndicatorElement: setIndicatorTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
3277 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3279 * @return {string} Symbolic name of indicator
3281 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
3282 return this.indicator
;
3286 * Get the indicator title.
3288 * The title is displayed when a user moves the mouse over the indicator.
3290 * @return {string} Indicator title text
3293 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
3294 return this.indicatorTitle
;
3298 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3299 * additional functionality to an element created by another class. The class provides
3300 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3301 * which are used to customize the look and feel of a widget to better describe its
3302 * importance and functionality.
3304 * The library currently contains the following styling flags for general use:
3306 * - **progressive**: Progressive styling is applied to convey that the widget will move the user
3307 * forward in a process.
3308 * - **destructive**: Destructive styling is applied to convey that the widget will remove
3311 * The flags affect the appearance of the buttons:
3314 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3315 * var button1 = new OO.ui.ButtonWidget( {
3316 * label: 'Progressive',
3317 * flags: 'progressive'
3319 * button2 = new OO.ui.ButtonWidget( {
3320 * label: 'Destructive',
3321 * flags: 'destructive'
3323 * $( document.body ).append( button1.$element, button2.$element );
3325 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an
3326 * action, use these flags: **primary** and **safe**.
3327 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3329 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3335 * @param {Object} [config] Configuration options
3336 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary')
3338 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3339 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3340 * @cfg {jQuery} [$flagged] The flagged element. By default,
3341 * the flagged functionality is applied to the element created by the class ($element).
3342 * If a different element is specified, the flagged functionality will be applied to it instead.
3344 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3345 // Configuration initialization
3346 config
= config
|| {};
3350 this.$flagged
= null;
3353 this.setFlags( config
.flags
);
3354 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3361 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3362 * parameter contains the name of each modified flag and indicates whether it was
3365 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3366 * that the flag was added, `false` that the flag was removed.
3372 * Set the flagged element.
3374 * This method is used to retarget a flagged mixin so that its functionality applies to the
3375 * specified element.
3376 * If an element is already set, the method will remove the mixin’s effect on that element.
3378 * @param {jQuery} $flagged Element that should be flagged
3380 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3381 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3382 return 'oo-ui-flaggedElement-' + flag
;
3385 if ( this.$flagged
) {
3386 this.$flagged
.removeClass( classNames
);
3389 this.$flagged
= $flagged
.addClass( classNames
);
3393 * Check if the specified flag is set.
3395 * @param {string} flag Name of flag
3396 * @return {boolean} The flag is set
3398 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3399 // This may be called before the constructor, thus before this.flags is set
3400 return this.flags
&& ( flag
in this.flags
);
3404 * Get the names of all flags set.
3406 * @return {string[]} Flag names
3408 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3409 // This may be called before the constructor, thus before this.flags is set
3410 return Object
.keys( this.flags
|| {} );
3417 * @return {OO.ui.Element} The element, for chaining
3420 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3421 var flag
, className
,
3424 classPrefix
= 'oo-ui-flaggedElement-';
3426 for ( flag
in this.flags
) {
3427 className
= classPrefix
+ flag
;
3428 changes
[ flag
] = false;
3429 delete this.flags
[ flag
];
3430 remove
.push( className
);
3433 if ( this.$flagged
) {
3434 this.$flagged
.removeClass( remove
);
3437 this.updateThemeClasses();
3438 this.emit( 'flag', changes
);
3444 * Add one or more flags.
3446 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3447 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3448 * be added (`true`) or removed (`false`).
3450 * @return {OO.ui.Element} The element, for chaining
3453 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3454 var i
, len
, flag
, className
,
3458 classPrefix
= 'oo-ui-flaggedElement-';
3460 if ( typeof flags
=== 'string' ) {
3461 className
= classPrefix
+ flags
;
3463 if ( !this.flags
[ flags
] ) {
3464 this.flags
[ flags
] = true;
3465 add
.push( className
);
3467 } else if ( Array
.isArray( flags
) ) {
3468 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3470 className
= classPrefix
+ flag
;
3472 if ( !this.flags
[ flag
] ) {
3473 changes
[ flag
] = true;
3474 this.flags
[ flag
] = true;
3475 add
.push( className
);
3478 } else if ( OO
.isPlainObject( flags
) ) {
3479 for ( flag
in flags
) {
3480 className
= classPrefix
+ flag
;
3481 if ( flags
[ flag
] ) {
3483 if ( !this.flags
[ flag
] ) {
3484 changes
[ flag
] = true;
3485 this.flags
[ flag
] = true;
3486 add
.push( className
);
3490 if ( this.flags
[ flag
] ) {
3491 changes
[ flag
] = false;
3492 delete this.flags
[ flag
];
3493 remove
.push( className
);
3499 if ( this.$flagged
) {
3502 .removeClass( remove
);
3505 this.updateThemeClasses();
3506 this.emit( 'flag', changes
);
3512 * TitledElement is mixed into other classes to provide a `title` attribute.
3513 * Titles are rendered by the browser and are made visible when the user moves
3514 * the mouse over the element. Titles are not visible on touch devices.
3517 * // TitledElement provides a `title` attribute to the
3518 * // ButtonWidget class.
3519 * var button = new OO.ui.ButtonWidget( {
3520 * label: 'Button with Title',
3521 * title: 'I am a button'
3523 * $( document.body ).append( button.$element );
3529 * @param {Object} [config] Configuration options
3530 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3531 * If this config is omitted, the title functionality is applied to $element, the
3532 * element created by the class.
3533 * @cfg {string|Function} [title] The title text or a function that returns text. If
3534 * this config is omitted, the value of the {@link #static-title static title} property is used.
3536 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3537 // Configuration initialization
3538 config
= config
|| {};
3541 this.$titled
= null;
3545 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3546 this.setTitledElement( config
.$titled
|| this.$element
);
3551 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3553 /* Static Properties */
3556 * The title text, a function that returns text, or `null` for no title. The value of the static
3557 * property is overridden if the #title config option is used.
3561 * @property {string|Function|null}
3563 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3568 * Set the titled element.
3570 * This method is used to retarget a TitledElement mixin so that its functionality applies to the
3571 * specified element.
3572 * If an element is already set, the mixin’s effect on that element is removed before the new
3573 * element is set up.
3575 * @param {jQuery} $titled Element that should use the 'titled' functionality
3577 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3578 if ( this.$titled
) {
3579 this.$titled
.removeAttr( 'title' );
3582 this.$titled
= $titled
;
3591 * @param {string|Function|null} title Title text, a function that returns text, or `null`
3594 * @return {OO.ui.Element} The element, for chaining
3596 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3597 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3598 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3600 if ( this.title
!== title
) {
3609 * Update the title attribute, in case of changes to title or accessKey.
3613 * @return {OO.ui.Element} The element, for chaining
3615 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3616 var title
= this.getTitle();
3617 if ( this.$titled
) {
3618 if ( title
!== null ) {
3619 // Only if this is an AccessKeyedElement
3620 if ( this.formatTitleWithAccessKey
) {
3621 title
= this.formatTitleWithAccessKey( title
);
3623 this.$titled
.attr( 'title', title
);
3625 this.$titled
.removeAttr( 'title' );
3634 * @return {string} Title string
3636 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3641 * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
3642 * Access keys allow an user to go to a specific element by using
3643 * a shortcut combination of a browser specific keys + the key
3647 * // AccessKeyedElement provides an `accesskey` attribute to the
3648 * // ButtonWidget class.
3649 * var button = new OO.ui.ButtonWidget( {
3650 * label: 'Button with access key',
3653 * $( document.body ).append( button.$element );
3659 * @param {Object} [config] Configuration options
3660 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3661 * If this config is omitted, the access key functionality is applied to $element, the
3662 * element created by the class.
3663 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3664 * this config is omitted, no access key will be added.
3666 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3667 // Configuration initialization
3668 config
= config
|| {};
3671 this.$accessKeyed
= null;
3672 this.accessKey
= null;
3675 this.setAccessKey( config
.accessKey
|| null );
3676 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3678 // If this is also a TitledElement and it initialized before we did, we may have
3679 // to update the title with the access key
3680 if ( this.updateTitle
) {
3687 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3689 /* Static Properties */
3692 * The access key, a function that returns a key, or `null` for no access key.
3696 * @property {string|Function|null}
3698 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3703 * Set the access keyed element.
3705 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to
3706 * the specified element.
3707 * If an element is already set, the mixin's effect on that element is removed before the new
3708 * element is set up.
3710 * @param {jQuery} $accessKeyed Element that should use the 'access keyed' functionality
3712 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3713 if ( this.$accessKeyed
) {
3714 this.$accessKeyed
.removeAttr( 'accesskey' );
3717 this.$accessKeyed
= $accessKeyed
;
3718 if ( this.accessKey
) {
3719 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3726 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no
3729 * @return {OO.ui.Element} The element, for chaining
3731 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3732 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3734 if ( this.accessKey
!== accessKey
) {
3735 if ( this.$accessKeyed
) {
3736 if ( accessKey
!== null ) {
3737 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3739 this.$accessKeyed
.removeAttr( 'accesskey' );
3742 this.accessKey
= accessKey
;
3744 // Only if this is a TitledElement
3745 if ( this.updateTitle
) {
3756 * @return {string} accessKey string
3758 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3759 return this.accessKey
;
3763 * Add information about the access key to the element's tooltip label.
3764 * (This is only public for hacky usage in FieldLayout.)
3766 * @param {string} title Tooltip label for `title` attribute
3769 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3772 if ( !this.$accessKeyed
) {
3773 // Not initialized yet; the constructor will call updateTitle() which will rerun this
3777 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the
3779 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3780 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3782 accessKey
= this.getAccessKey();
3785 title
+= ' [' + accessKey
+ ']';
3791 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3792 * feels, and functionality can be customized via the class’s configuration options
3793 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3796 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3799 * // A button widget.
3800 * var button = new OO.ui.ButtonWidget( {
3801 * label: 'Button with Icon',
3805 * $( document.body ).append( button.$element );
3807 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3810 * @extends OO.ui.Widget
3811 * @mixins OO.ui.mixin.ButtonElement
3812 * @mixins OO.ui.mixin.IconElement
3813 * @mixins OO.ui.mixin.IndicatorElement
3814 * @mixins OO.ui.mixin.LabelElement
3815 * @mixins OO.ui.mixin.TitledElement
3816 * @mixins OO.ui.mixin.FlaggedElement
3817 * @mixins OO.ui.mixin.TabIndexedElement
3818 * @mixins OO.ui.mixin.AccessKeyedElement
3821 * @param {Object} [config] Configuration options
3822 * @cfg {boolean} [active=false] Whether button should be shown as active
3823 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3824 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3825 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3827 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3828 // Configuration initialization
3829 config
= config
|| {};
3831 // Parent constructor
3832 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3834 // Mixin constructors
3835 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3836 OO
.ui
.mixin
.IconElement
.call( this, config
);
3837 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3838 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3839 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, {
3840 $titled
: this.$button
3842 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3843 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, {
3844 $tabIndexed
: this.$button
3846 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, {
3847 $accessKeyed
: this.$button
3853 this.noFollow
= false;
3856 this.connect( this, {
3857 disable
: 'onDisable'
3861 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3863 .addClass( 'oo-ui-buttonWidget' )
3864 .append( this.$button
);
3865 this.setActive( config
.active
);
3866 this.setHref( config
.href
);
3867 this.setTarget( config
.target
);
3868 this.setNoFollow( config
.noFollow
);
3873 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3874 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3875 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3876 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3877 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3878 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3879 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3880 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3881 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3883 /* Static Properties */
3889 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3895 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3900 * Get hyperlink location.
3902 * @return {string} Hyperlink location
3904 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3909 * Get hyperlink target.
3911 * @return {string} Hyperlink target
3913 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3918 * Get search engine traversal hint.
3920 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3922 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3923 return this.noFollow
;
3927 * Set hyperlink location.
3929 * @param {string|null} href Hyperlink location, null to remove
3931 * @return {OO.ui.Widget} The widget, for chaining
3933 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3934 href
= typeof href
=== 'string' ? href
: null;
3935 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3939 if ( href
!== this.href
) {
3948 * Update the `href` attribute, in case of changes to href or
3953 * @return {OO.ui.Widget} The widget, for chaining
3955 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3956 if ( this.href
!== null && !this.isDisabled() ) {
3957 this.$button
.attr( 'href', this.href
);
3959 this.$button
.removeAttr( 'href' );
3966 * Handle disable events.
3969 * @param {boolean} disabled Element is disabled
3971 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3976 * Set hyperlink target.
3978 * @param {string|null} target Hyperlink target, null to remove
3979 * @return {OO.ui.Widget} The widget, for chaining
3981 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3982 target
= typeof target
=== 'string' ? target
: null;
3984 if ( target
!== this.target
) {
3985 this.target
= target
;
3986 if ( target
!== null ) {
3987 this.$button
.attr( 'target', target
);
3989 this.$button
.removeAttr( 'target' );
3997 * Set search engine traversal hint.
3999 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
4000 * @return {OO.ui.Widget} The widget, for chaining
4002 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
4003 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
4005 if ( noFollow
!== this.noFollow
) {
4006 this.noFollow
= noFollow
;
4008 this.$button
.attr( 'rel', 'nofollow' );
4010 this.$button
.removeAttr( 'rel' );
4017 // Override method visibility hints from ButtonElement
4028 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
4029 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
4030 * removed, and cleared from the group.
4033 * // A ButtonGroupWidget with two buttons.
4034 * var button1 = new OO.ui.PopupButtonWidget( {
4035 * label: 'Select a category',
4038 * $content: $( '<p>List of categories…</p>' ),
4043 * button2 = new OO.ui.ButtonWidget( {
4046 * buttonGroup = new OO.ui.ButtonGroupWidget( {
4047 * items: [ button1, button2 ]
4049 * $( document.body ).append( buttonGroup.$element );
4052 * @extends OO.ui.Widget
4053 * @mixins OO.ui.mixin.GroupElement
4054 * @mixins OO.ui.mixin.TitledElement
4057 * @param {Object} [config] Configuration options
4058 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
4060 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
4061 // Configuration initialization
4062 config
= config
|| {};
4064 // Parent constructor
4065 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
4067 // Mixin constructors
4068 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, {
4069 $group
: this.$element
4071 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4074 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
4075 if ( Array
.isArray( config
.items
) ) {
4076 this.addItems( config
.items
);
4082 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
4083 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
4084 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.TitledElement
);
4086 /* Static Properties */
4092 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
4100 * @return {OO.ui.Widget} The widget, for chaining
4102 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
4103 if ( !this.isDisabled() ) {
4104 if ( this.items
[ 0 ] ) {
4105 this.items
[ 0 ].focus();
4114 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
4119 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}.
4120 * In general, IconWidgets should be used with OO.ui.LabelWidget, which creates a label that
4121 * identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
4122 * for a list of icons included in the library.
4125 * // An IconWidget with a label via LabelWidget.
4126 * var myIcon = new OO.ui.IconWidget( {
4130 * // Create a label.
4131 * iconLabel = new OO.ui.LabelWidget( {
4134 * $( document.body ).append( myIcon.$element, iconLabel.$element );
4136 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4139 * @extends OO.ui.Widget
4140 * @mixins OO.ui.mixin.IconElement
4141 * @mixins OO.ui.mixin.TitledElement
4142 * @mixins OO.ui.mixin.LabelElement
4143 * @mixins OO.ui.mixin.FlaggedElement
4146 * @param {Object} [config] Configuration options
4148 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
4149 // Configuration initialization
4150 config
= config
|| {};
4152 // Parent constructor
4153 OO
.ui
.IconWidget
.parent
.call( this, config
);
4155 // Mixin constructors
4156 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, {
4157 $icon
: this.$element
4159 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, {
4160 $titled
: this.$element
4162 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
4163 $label
: this.$element
,
4164 invisibleLabel
: true
4166 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, {
4167 $flagged
: this.$element
4171 this.$element
.addClass( 'oo-ui-iconWidget' );
4172 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4173 // nested in other widgets, because this widget used to not mix in LabelElement.
4174 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4179 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
4180 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
4181 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
4182 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.LabelElement
);
4183 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
4185 /* Static Properties */
4191 OO
.ui
.IconWidget
.static.tagName
= 'span';
4194 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4195 * attention to the status of an item or to clarify the function within a control. For a list of
4196 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4199 * // An indicator widget.
4200 * var indicator1 = new OO.ui.IndicatorWidget( {
4201 * indicator: 'required'
4203 * // Create a fieldset layout to add a label.
4204 * fieldset = new OO.ui.FieldsetLayout();
4205 * fieldset.addItems( [
4206 * new OO.ui.FieldLayout( indicator1, {
4207 * label: 'A required indicator:'
4210 * $( document.body ).append( fieldset.$element );
4212 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4215 * @extends OO.ui.Widget
4216 * @mixins OO.ui.mixin.IndicatorElement
4217 * @mixins OO.ui.mixin.TitledElement
4218 * @mixins OO.ui.mixin.LabelElement
4221 * @param {Object} [config] Configuration options
4223 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4224 // Configuration initialization
4225 config
= config
|| {};
4227 // Parent constructor
4228 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4230 // Mixin constructors
4231 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, {
4232 $indicator
: this.$element
4234 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, {
4235 $titled
: this.$element
4237 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
4238 $label
: this.$element
,
4239 invisibleLabel
: true
4243 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4244 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4245 // nested in other widgets, because this widget used to not mix in LabelElement.
4246 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4251 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4252 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4253 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4254 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.LabelElement
);
4256 /* Static Properties */
4262 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4265 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4266 * be configured with a `label` option that is set to a string, a label node, or a function:
4268 * - String: a plaintext string
4269 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4270 * label that includes a link or special styling, such as a gray color or additional
4271 * graphical elements.
4272 * - Function: a function that will produce a string in the future. Functions are used
4273 * in cases where the value of the label is not currently defined.
4275 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget},
4276 * which will come into focus when the label is clicked.
4279 * // Two LabelWidgets.
4280 * var label1 = new OO.ui.LabelWidget( {
4281 * label: 'plaintext label'
4283 * label2 = new OO.ui.LabelWidget( {
4284 * label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4286 * // Create a fieldset layout with fields for each example.
4287 * fieldset = new OO.ui.FieldsetLayout();
4288 * fieldset.addItems( [
4289 * new OO.ui.FieldLayout( label1 ),
4290 * new OO.ui.FieldLayout( label2 )
4292 * $( document.body ).append( fieldset.$element );
4295 * @extends OO.ui.Widget
4296 * @mixins OO.ui.mixin.LabelElement
4297 * @mixins OO.ui.mixin.TitledElement
4300 * @param {Object} [config] Configuration options
4301 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4302 * Clicking the label will focus the specified input field.
4304 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4305 // Configuration initialization
4306 config
= config
|| {};
4308 // Parent constructor
4309 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4311 // Mixin constructors
4312 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
4313 $label
: this.$element
4315 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4318 this.input
= config
.input
;
4322 if ( this.input
.getInputId() ) {
4323 this.$element
.attr( 'for', this.input
.getInputId() );
4325 this.$label
.on( 'click', function () {
4326 this.input
.simulateLabelClick();
4330 this.$element
.addClass( 'oo-ui-labelWidget' );
4335 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4336 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4337 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4339 /* Static Properties */
4345 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4348 * PendingElement is a mixin that is used to create elements that notify users that something is
4349 * happening and that they should wait before proceeding. The pending state is visually represented
4350 * with a pending texture that appears in the head of a pending
4351 * {@link OO.ui.ProcessDialog process dialog} or in the input field of a
4352 * {@link OO.ui.TextInputWidget text input widget}.
4354 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked
4355 * as pending, but only when used in {@link OO.ui.MessageDialog message dialogs}. The behavior is
4356 * not currently supported for action widgets used in process dialogs.
4359 * function MessageDialog( config ) {
4360 * MessageDialog.parent.call( this, config );
4362 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4364 * MessageDialog.static.name = 'myMessageDialog';
4365 * MessageDialog.static.actions = [
4366 * { action: 'save', label: 'Done', flags: 'primary' },
4367 * { label: 'Cancel', flags: 'safe' }
4370 * MessageDialog.prototype.initialize = function () {
4371 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4372 * this.content = new OO.ui.PanelLayout( { padded: true } );
4373 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending ' +
4374 * 'state. Note that action widgets can be marked pending in message dialogs but not ' +
4375 * 'process dialogs.</p>' );
4376 * this.$body.append( this.content.$element );
4378 * MessageDialog.prototype.getBodyHeight = function () {
4381 * MessageDialog.prototype.getActionProcess = function ( action ) {
4382 * var dialog = this;
4383 * if ( action === 'save' ) {
4384 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4385 * return new OO.ui.Process()
4387 * .next( function () {
4388 * dialog.getActions().get({actions: 'save'})[0].popPending();
4391 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4394 * var windowManager = new OO.ui.WindowManager();
4395 * $( document.body ).append( windowManager.$element );
4397 * var dialog = new MessageDialog();
4398 * windowManager.addWindows( [ dialog ] );
4399 * windowManager.openWindow( dialog );
4405 * @param {Object} [config] Configuration options
4406 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4408 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4409 // Configuration initialization
4410 config
= config
|| {};
4414 this.$pending
= null;
4417 this.setPendingElement( config
.$pending
|| this.$element
);
4422 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4427 * Set the pending element (and clean up any existing one).
4429 * @param {jQuery} $pending The element to set to pending.
4431 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4432 if ( this.$pending
) {
4433 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4436 this.$pending
= $pending
;
4437 if ( this.pending
> 0 ) {
4438 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4443 * Check if an element is pending.
4445 * @return {boolean} Element is pending
4447 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4448 return !!this.pending
;
4452 * Increase the pending counter. The pending state will remain active until the counter is zero
4453 * (i.e., the number of calls to #pushPending and #popPending is the same).
4456 * @return {OO.ui.Element} The element, for chaining
4458 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4459 if ( this.pending
=== 0 ) {
4460 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4461 this.updateThemeClasses();
4469 * Decrease the pending counter. The pending state will remain active until the counter is zero
4470 * (i.e., the number of calls to #pushPending and #popPending is the same).
4473 * @return {OO.ui.Element} The element, for chaining
4475 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4476 if ( this.pending
=== 1 ) {
4477 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4478 this.updateThemeClasses();
4480 this.pending
= Math
.max( 0, this.pending
- 1 );
4486 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4487 * in the document (for example, in an OO.ui.Window's $overlay).
4489 * The elements's position is automatically calculated and maintained when window is resized or the
4490 * page is scrolled. If you reposition the container manually, you have to call #position to make
4491 * sure the element is still placed correctly.
4493 * As positioning is only possible when both the element and the container are attached to the DOM
4494 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4495 * the #toggle method to display a floating popup, for example.
4501 * @param {Object} [config] Configuration options
4502 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4503 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4504 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4505 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4506 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4507 * 'top': Align the top edge with $floatableContainer's top edge
4508 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4509 * 'center': Vertically align the center with $floatableContainer's center
4510 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4511 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4512 * 'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
4513 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4514 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4515 * 'center': Horizontally align the center with $floatableContainer's center
4516 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4519 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4520 // Configuration initialization
4521 config
= config
|| {};
4524 this.$floatable
= null;
4525 this.$floatableContainer
= null;
4526 this.$floatableWindow
= null;
4527 this.$floatableClosestScrollable
= null;
4528 this.floatableOutOfView
= false;
4529 this.onFloatableScrollHandler
= this.position
.bind( this );
4530 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4533 this.setFloatableContainer( config
.$floatableContainer
);
4534 this.setFloatableElement( config
.$floatable
|| this.$element
);
4535 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4536 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4537 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ?
4538 true : !!config
.hideWhenOutOfView
;
4544 * Set floatable element.
4546 * If an element is already set, it will be cleaned up before setting up the new element.
4548 * @param {jQuery} $floatable Element to make floatable
4550 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4551 if ( this.$floatable
) {
4552 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4553 this.$floatable
.css( { left
: '', top
: '' } );
4556 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4561 * Set floatable container.
4563 * The element will be positioned relative to the specified container.
4565 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4567 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4568 this.$floatableContainer
= $floatableContainer
;
4569 if ( this.$floatable
) {
4575 * Change how the element is positioned vertically.
4577 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4579 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4580 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4581 throw new Error( 'Invalid value for vertical position: ' + position
);
4583 if ( this.verticalPosition
!== position
) {
4584 this.verticalPosition
= position
;
4585 if ( this.$floatable
) {
4592 * Change how the element is positioned horizontally.
4594 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4596 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4597 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4598 throw new Error( 'Invalid value for horizontal position: ' + position
);
4600 if ( this.horizontalPosition
!== position
) {
4601 this.horizontalPosition
= position
;
4602 if ( this.$floatable
) {
4609 * Toggle positioning.
4611 * Do not turn positioning on until after the element is attached to the DOM and visible.
4613 * @param {boolean} [positioning] Enable positioning, omit to toggle
4615 * @return {OO.ui.Element} The element, for chaining
4617 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4618 var closestScrollableOfContainer
;
4620 if ( !this.$floatable
|| !this.$floatableContainer
) {
4624 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4626 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4627 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4628 this.warnedUnattached
= true;
4631 if ( this.positioning
!== positioning
) {
4632 this.positioning
= positioning
;
4634 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer(
4635 this.$floatableContainer
[ 0 ]
4637 // If the scrollable is the root, we have to listen to scroll events
4638 // on the window because of browser inconsistencies.
4639 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4640 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow(
4641 closestScrollableOfContainer
4645 if ( positioning
) {
4646 this.$floatableWindow
= $( this.getElementWindow() );
4647 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4649 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4650 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4652 // Initial position after visible
4655 if ( this.$floatableWindow
) {
4656 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4657 this.$floatableWindow
= null;
4660 if ( this.$floatableClosestScrollable
) {
4661 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4662 this.$floatableClosestScrollable
= null;
4665 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4673 * Check whether the bottom edge of the given element is within the viewport of the given
4677 * @param {jQuery} $element
4678 * @param {jQuery} $container
4681 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4682 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
,
4683 rightEdgeInBounds
, startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4684 direction
= $element
.css( 'direction' );
4686 elemRect
= $element
[ 0 ].getBoundingClientRect();
4687 if ( $container
[ 0 ] === window
) {
4688 viewportSpacing
= OO
.ui
.getViewportSpacing();
4692 right
: document
.documentElement
.clientWidth
,
4693 bottom
: document
.documentElement
.clientHeight
4695 contRect
.top
+= viewportSpacing
.top
;
4696 contRect
.left
+= viewportSpacing
.left
;
4697 contRect
.right
-= viewportSpacing
.right
;
4698 contRect
.bottom
-= viewportSpacing
.bottom
;
4700 contRect
= $container
[ 0 ].getBoundingClientRect();
4703 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4704 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4705 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4706 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4707 if ( direction
=== 'rtl' ) {
4708 startEdgeInBounds
= rightEdgeInBounds
;
4709 endEdgeInBounds
= leftEdgeInBounds
;
4711 startEdgeInBounds
= leftEdgeInBounds
;
4712 endEdgeInBounds
= rightEdgeInBounds
;
4715 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4718 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4721 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4724 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4728 // The other positioning values are all about being inside the container,
4729 // so in those cases all we care about is that any part of the container is visible.
4730 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4731 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4735 * Check if the floatable is hidden to the user because it was offscreen.
4737 * @return {boolean} Floatable is out of view
4739 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4740 return this.floatableOutOfView
;
4744 * Position the floatable below its container.
4746 * This should only be done when both of them are attached to the DOM and visible.
4749 * @return {OO.ui.Element} The element, for chaining
4751 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4752 if ( !this.positioning
) {
4757 // To continue, some things need to be true:
4758 // The element must actually be in the DOM
4759 this.isElementAttached() && (
4760 // The closest scrollable is the current window
4761 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4762 // OR is an element in the element's DOM
4763 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4766 // Abort early if important parts of the widget are no longer attached to the DOM
4770 this.floatableOutOfView
= this.hideWhenOutOfView
&&
4771 !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4772 if ( this.floatableOutOfView
) {
4773 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4776 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4779 this.$floatable
.css( this.computePosition() );
4781 // We updated the position, so re-evaluate the clipping state.
4782 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4783 // will not notice the need to update itself.)
4784 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here.
4785 // Why does it not listen to the right events in the right places?
4794 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4795 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4796 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4798 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4800 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4801 var isBody
, scrollableX
, scrollableY
, containerPos
,
4802 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4803 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4804 direction
= this.$floatableContainer
.css( 'direction' ),
4805 $offsetParent
= this.$floatable
.offsetParent();
4807 if ( $offsetParent
.is( 'html' ) ) {
4808 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4809 // <html> element, but they do work on the <body>
4810 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4812 isBody
= $offsetParent
.is( 'body' );
4813 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' ||
4814 $offsetParent
.css( 'overflow-x' ) === 'auto';
4815 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' ||
4816 $offsetParent
.css( 'overflow-y' ) === 'auto';
4818 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4819 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4820 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container
4821 // is the body, or if it isn't scrollable
4822 scrollTop
= scrollableY
&& !isBody
?
4823 $offsetParent
.scrollTop() : 0;
4824 scrollLeft
= scrollableX
&& !isBody
?
4825 OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4827 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4828 // if the <body> has a margin
4829 containerPos
= isBody
?
4830 this.$floatableContainer
.offset() :
4831 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4832 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4833 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4834 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4835 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4837 if ( this.verticalPosition
=== 'below' ) {
4838 newPos
.top
= containerPos
.bottom
;
4839 } else if ( this.verticalPosition
=== 'above' ) {
4840 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4841 } else if ( this.verticalPosition
=== 'top' ) {
4842 newPos
.top
= containerPos
.top
;
4843 } else if ( this.verticalPosition
=== 'bottom' ) {
4844 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4845 } else if ( this.verticalPosition
=== 'center' ) {
4846 newPos
.top
= containerPos
.top
+
4847 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4850 if ( this.horizontalPosition
=== 'before' ) {
4851 newPos
.end
= containerPos
.start
;
4852 } else if ( this.horizontalPosition
=== 'after' ) {
4853 newPos
.start
= containerPos
.end
;
4854 } else if ( this.horizontalPosition
=== 'start' ) {
4855 newPos
.start
= containerPos
.start
;
4856 } else if ( this.horizontalPosition
=== 'end' ) {
4857 newPos
.end
= containerPos
.end
;
4858 } else if ( this.horizontalPosition
=== 'center' ) {
4859 newPos
.left
= containerPos
.left
+
4860 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4863 if ( newPos
.start
!== undefined ) {
4864 if ( direction
=== 'rtl' ) {
4865 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4866 $offsetParent
).outerWidth() - newPos
.start
;
4868 newPos
.left
= newPos
.start
;
4870 delete newPos
.start
;
4872 if ( newPos
.end
!== undefined ) {
4873 if ( direction
=== 'rtl' ) {
4874 newPos
.left
= newPos
.end
;
4876 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) :
4877 $offsetParent
).outerWidth() - newPos
.end
;
4882 // Account for scroll position
4883 if ( newPos
.top
!== '' ) {
4884 newPos
.top
+= scrollTop
;
4886 if ( newPos
.bottom
!== '' ) {
4887 newPos
.bottom
-= scrollTop
;
4889 if ( newPos
.left
!== '' ) {
4890 newPos
.left
+= scrollLeft
;
4892 if ( newPos
.right
!== '' ) {
4893 newPos
.right
-= scrollLeft
;
4896 // Account for scrollbar gutter
4897 if ( newPos
.bottom
!== '' ) {
4898 newPos
.bottom
-= horizScrollbarHeight
;
4900 if ( direction
=== 'rtl' ) {
4901 if ( newPos
.left
!== '' ) {
4902 newPos
.left
-= vertScrollbarWidth
;
4905 if ( newPos
.right
!== '' ) {
4906 newPos
.right
-= vertScrollbarWidth
;
4914 * Element that can be automatically clipped to visible boundaries.
4916 * Whenever the element's natural height changes, you have to call
4917 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4918 * clipping correctly.
4920 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4921 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4922 * then #$clippable will be given a fixed reduced height and/or width and will be made
4923 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4924 * but you can build a static footer by setting #$clippableContainer to an element that contains
4925 * #$clippable and the footer.
4931 * @param {Object} [config] Configuration options
4932 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4933 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4934 * omit to use #$clippable
4936 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4937 // Configuration initialization
4938 config
= config
|| {};
4941 this.$clippable
= null;
4942 this.$clippableContainer
= null;
4943 this.clipping
= false;
4944 this.clippedHorizontally
= false;
4945 this.clippedVertically
= false;
4946 this.$clippableScrollableContainer
= null;
4947 this.$clippableScroller
= null;
4948 this.$clippableWindow
= null;
4949 this.idealWidth
= null;
4950 this.idealHeight
= null;
4951 this.onClippableScrollHandler
= this.clip
.bind( this );
4952 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4955 if ( config
.$clippableContainer
) {
4956 this.setClippableContainer( config
.$clippableContainer
);
4958 this.setClippableElement( config
.$clippable
|| this.$element
);
4964 * Set clippable element.
4966 * If an element is already set, it will be cleaned up before setting up the new element.
4968 * @param {jQuery} $clippable Element to make clippable
4970 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4971 if ( this.$clippable
) {
4972 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4973 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4974 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4977 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4982 * Set clippable container.
4984 * This is the container that will be measured when deciding whether to clip. When clipping,
4985 * #$clippable will be resized in order to keep the clippable container fully visible.
4987 * If the clippable container is unset, #$clippable will be used.
4989 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4991 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4992 this.$clippableContainer
= $clippableContainer
;
4993 if ( this.$clippable
) {
5001 * Do not turn clipping on until after the element is attached to the DOM and visible.
5003 * @param {boolean} [clipping] Enable clipping, omit to toggle
5005 * @return {OO.ui.Element} The element, for chaining
5007 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
5008 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
5010 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5011 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
5012 this.warnedUnattached
= true;
5015 if ( this.clipping
!== clipping
) {
5016 this.clipping
= clipping
;
5018 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
5019 // If the clippable container is the root, we have to listen to scroll events and check
5020 // jQuery.scrollTop on the window because of browser inconsistencies
5021 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
5022 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
5023 this.$clippableScrollableContainer
;
5024 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
5025 this.$clippableWindow
= $( this.getElementWindow() )
5026 .on( 'resize', this.onClippableWindowResizeHandler
);
5027 // Initial clip after visible
5030 this.$clippable
.css( {
5038 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5040 this.$clippableScrollableContainer
= null;
5041 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
5042 this.$clippableScroller
= null;
5043 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
5044 this.$clippableWindow
= null;
5052 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5054 * @return {boolean} Element will be clipped to the visible area
5056 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
5057 return this.clipping
;
5061 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5063 * @return {boolean} Part of the element is being clipped
5065 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
5066 return this.clippedHorizontally
|| this.clippedVertically
;
5070 * Check if the right of the element is being clipped by the nearest scrollable container.
5072 * @return {boolean} Part of the element is being clipped
5074 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
5075 return this.clippedHorizontally
;
5079 * Check if the bottom of the element is being clipped by the nearest scrollable container.
5081 * @return {boolean} Part of the element is being clipped
5083 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
5084 return this.clippedVertically
;
5088 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
5090 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
5091 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
5093 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
5094 this.idealWidth
= width
;
5095 this.idealHeight
= height
;
5097 if ( !this.clipping
) {
5098 // Update dimensions
5099 this.$clippable
.css( { width
: width
, height
: height
} );
5101 // While clipping, idealWidth and idealHeight are not considered
5105 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5106 * ClippableElement will clip the opposite side when reducing element's width.
5108 * Classes that mix in ClippableElement should override this to return 'right' if their
5109 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
5110 * If your class also mixes in FloatableElement, this is handled automatically.
5112 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5113 * always in pixels, even if they were unset or set to 'auto'.)
5115 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
5117 * @return {string} 'left' or 'right'
5119 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
5120 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
5127 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5128 * ClippableElement will clip the opposite side when reducing element's width.
5130 * Classes that mix in ClippableElement should override this to return 'bottom' if their
5131 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
5132 * If your class also mixes in FloatableElement, this is handled automatically.
5134 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5135 * always in pixels, even if they were unset or set to 'auto'.)
5137 * When in doubt, 'top' is a sane fallback.
5139 * @return {string} 'top' or 'bottom'
5141 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
5142 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
5149 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5150 * when the element's natural height changes.
5152 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5153 * overlapped by, the visible area of the nearest scrollable container.
5155 * Because calling clip() when the natural height changes isn't always possible, we also set
5156 * max-height when the element isn't being clipped. This means that if the element tries to grow
5157 * beyond the edge, something reasonable will happen before clip() is called.
5160 * @return {OO.ui.Element} The element, for chaining
5162 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
5163 var extraHeight
, extraWidth
, viewportSpacing
,
5164 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
5165 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
5166 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
5167 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
5168 // Extra tolerance so that the sloppy code below doesn't result in results that are off
5169 // by one or two pixels. (And also so that we have space to display drop shadows.)
5170 // Chosen by fair dice roll.
5173 if ( !this.clipping
) {
5174 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below
5179 function rectIntersection( a
, b
) {
5181 out
.top
= Math
.max( a
.top
, b
.top
);
5182 out
.left
= Math
.max( a
.left
, b
.left
);
5183 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
5184 out
.right
= Math
.min( a
.right
, b
.right
);
5188 viewportSpacing
= OO
.ui
.getViewportSpacing();
5190 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
5191 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
5192 // Dimensions of the browser window, rather than the element!
5196 right
: document
.documentElement
.clientWidth
,
5197 bottom
: document
.documentElement
.clientHeight
5199 viewportRect
.top
+= viewportSpacing
.top
;
5200 viewportRect
.left
+= viewportSpacing
.left
;
5201 viewportRect
.right
-= viewportSpacing
.right
;
5202 viewportRect
.bottom
-= viewportSpacing
.bottom
;
5204 $viewport
= this.$clippableScrollableContainer
;
5205 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
5206 // Convert into a plain object
5207 viewportRect
= $.extend( {}, viewportRect
);
5210 // Account for scrollbar gutter
5211 direction
= $viewport
.css( 'direction' );
5212 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
5213 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
5214 viewportRect
.bottom
-= horizScrollbarHeight
;
5215 if ( direction
=== 'rtl' ) {
5216 viewportRect
.left
+= vertScrollbarWidth
;
5218 viewportRect
.right
-= vertScrollbarWidth
;
5221 // Add arbitrary tolerance
5222 viewportRect
.top
+= buffer
;
5223 viewportRect
.left
+= buffer
;
5224 viewportRect
.right
-= buffer
;
5225 viewportRect
.bottom
-= buffer
;
5227 $item
= this.$clippableContainer
|| this.$clippable
;
5229 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
5230 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
5232 itemRect
= $item
[ 0 ].getBoundingClientRect();
5233 // Convert into a plain object
5234 itemRect
= $.extend( {}, itemRect
);
5236 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5237 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5238 if ( this.getHorizontalAnchorEdge() === 'right' ) {
5239 itemRect
.left
= viewportRect
.left
;
5241 itemRect
.right
= viewportRect
.right
;
5243 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5244 itemRect
.top
= viewportRect
.top
;
5246 itemRect
.bottom
= viewportRect
.bottom
;
5249 availableRect
= rectIntersection( viewportRect
, itemRect
);
5251 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5252 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5253 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5254 desiredWidth
= Math
.min( desiredWidth
,
5255 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5256 desiredHeight
= Math
.min( desiredHeight
,
5257 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5258 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5259 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5260 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5261 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5262 clipWidth
= allotedWidth
< naturalWidth
;
5263 clipHeight
= allotedHeight
< naturalHeight
;
5266 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5268 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5270 this.$clippable
.css( 'overflowX', 'scroll' );
5271 // eslint-disable-next-line no-void
5272 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5273 this.$clippable
.css( {
5274 width
: Math
.max( 0, allotedWidth
),
5278 this.$clippable
.css( {
5280 width
: this.idealWidth
|| '',
5281 maxWidth
: Math
.max( 0, allotedWidth
)
5285 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars.
5287 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for
5289 this.$clippable
.css( 'overflowY', 'scroll' );
5290 // eslint-disable-next-line no-void
5291 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5292 this.$clippable
.css( {
5293 height
: Math
.max( 0, allotedHeight
),
5297 this.$clippable
.css( {
5299 height
: this.idealHeight
|| '',
5300 maxHeight
: Math
.max( 0, allotedHeight
)
5304 // If we stopped clipping in at least one of the dimensions
5305 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5306 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5309 this.clippedHorizontally
= clipWidth
;
5310 this.clippedVertically
= clipHeight
;
5316 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5317 * By default, each popup has an anchor that points toward its origin.
5318 * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
5320 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5324 * var popup = new OO.ui.PopupWidget( {
5325 * $content: $( '<p>Hi there!</p>' ),
5330 * $( document.body ).append( popup.$element );
5331 * // To display the popup, toggle the visibility to 'true'.
5332 * popup.toggle( true );
5334 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5337 * @extends OO.ui.Widget
5338 * @mixins OO.ui.mixin.LabelElement
5339 * @mixins OO.ui.mixin.ClippableElement
5340 * @mixins OO.ui.mixin.FloatableElement
5343 * @param {Object} [config] Configuration options
5344 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5345 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5346 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5347 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5348 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5349 * of $floatableContainer
5350 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5351 * of $floatableContainer
5352 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5353 * endwards (right/left) to the vertical center of $floatableContainer
5354 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5355 * startwards (left/right) to the vertical center of $floatableContainer
5356 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5357 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in
5358 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5359 * move the popup as far downwards as possible.
5360 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in
5361 * RTL) as possible while still keeping the anchor within the popup; if position is before/after,
5362 * move the popup as far upwards as possible.
5363 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the
5364 * center of the popup with the center of $floatableContainer.
5365 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5366 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5367 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5368 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5369 * desired direction to display the popup without clipping
5370 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5371 * See the [OOUI docs on MediaWiki][3] for an example.
5372 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5373 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a
5375 * @cfg {jQuery} [$content] Content to append to the popup's body
5376 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5377 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5378 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5379 * This config option is only relevant if #autoClose is set to `true`. See the
5380 * [OOUI documentation on MediaWiki][2] for an example.
5381 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5382 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5384 * @cfg {boolean} [padded=false] Add padding to the popup's body
5386 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5387 // Configuration initialization
5388 config
= config
|| {};
5390 // Parent constructor
5391 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5393 // Properties (must be set before ClippableElement constructor call)
5394 this.$body
= $( '<div>' );
5395 this.$popup
= $( '<div>' );
5397 // Mixin constructors
5398 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5399 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
5400 $clippable
: this.$body
,
5401 $clippableContainer
: this.$popup
5403 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5406 this.$anchor
= $( '<div>' );
5407 // If undefined, will be computed lazily in computePosition()
5408 this.$container
= config
.$container
;
5409 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5410 this.autoClose
= !!config
.autoClose
;
5411 this.transitionTimeout
= null;
5412 this.anchored
= false;
5413 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5414 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5417 this.setSize( config
.width
, config
.height
);
5418 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5419 this.setAlignment( config
.align
|| 'center' );
5420 this.setPosition( config
.position
|| 'below' );
5421 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5422 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5423 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5424 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5426 .addClass( 'oo-ui-popupWidget-popup' )
5427 .append( this.$body
);
5429 .addClass( 'oo-ui-popupWidget' )
5430 .append( this.$popup
, this.$anchor
);
5431 // Move content, which was added to #$element by OO.ui.Widget, to the body
5432 // FIXME This is gross, we should use '$body' or something for the config
5433 if ( config
.$content
instanceof $ ) {
5434 this.$body
.append( config
.$content
);
5437 if ( config
.padded
) {
5438 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5441 if ( config
.head
) {
5442 this.closeButton
= new OO
.ui
.ButtonWidget( {
5446 this.closeButton
.connect( this, {
5447 click
: 'onCloseButtonClick'
5449 this.$head
= $( '<div>' )
5450 .addClass( 'oo-ui-popupWidget-head' )
5451 .append( this.$label
, this.closeButton
.$element
);
5452 this.$popup
.prepend( this.$head
);
5455 if ( config
.$footer
) {
5456 this.$footer
= $( '<div>' )
5457 .addClass( 'oo-ui-popupWidget-footer' )
5458 .append( config
.$footer
);
5459 this.$popup
.append( this.$footer
);
5462 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5463 // that reference properties not initialized at that time of parent class construction
5464 // TODO: Find a better way to handle post-constructor setup
5465 this.visible
= false;
5466 this.$element
.addClass( 'oo-ui-element-hidden' );
5471 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5472 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5473 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5474 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5481 * The popup is ready: it is visible and has been positioned and clipped.
5487 * Handles document mouse down events.
5490 * @param {MouseEvent} e Mouse down event
5492 OO
.ui
.PopupWidget
.prototype.onDocumentMouseDown = function ( e
) {
5495 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5497 this.toggle( false );
5501 // Deprecated alias since 0.28.3
5502 OO
.ui
.PopupWidget
.prototype.onMouseDown = function () {
5503 OO
.ui
.warnDeprecation( 'onMouseDown is deprecated, use onDocumentMouseDown instead' );
5504 this.onDocumentMouseDown
.apply( this, arguments
);
5508 * Bind document mouse down listener.
5512 OO
.ui
.PopupWidget
.prototype.bindDocumentMouseDownListener = function () {
5513 // Capture clicks outside popup
5514 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5515 // We add 'click' event because iOS safari needs to respond to this event.
5516 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5517 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5518 // of occasionally not emitting 'click' properly, that event seems to be the standard
5519 // that it should be emitting, so we add it to this and will operate the event handler
5520 // on whichever of these events was triggered first
5521 this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5524 // Deprecated alias since 0.28.3
5525 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
5526 OO
.ui
.warnDeprecation( 'bindMouseDownListener is deprecated, use bindDocumentMouseDownListener instead' );
5527 this.bindDocumentMouseDownListener
.apply( this, arguments
);
5531 * Handles close button click events.
5535 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5536 if ( this.isVisible() ) {
5537 this.toggle( false );
5542 * Unbind document mouse down listener.
5546 OO
.ui
.PopupWidget
.prototype.unbindDocumentMouseDownListener = function () {
5547 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5548 this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5551 // Deprecated alias since 0.28.3
5552 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
5553 OO
.ui
.warnDeprecation( 'unbindMouseDownListener is deprecated, use unbindDocumentMouseDownListener instead' );
5554 this.unbindDocumentMouseDownListener
.apply( this, arguments
);
5558 * Handles document key down events.
5561 * @param {KeyboardEvent} e Key down event
5563 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5565 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5568 this.toggle( false );
5570 e
.stopPropagation();
5575 * Bind document key down listener.
5579 OO
.ui
.PopupWidget
.prototype.bindDocumentKeyDownListener = function () {
5580 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5583 // Deprecated alias since 0.28.3
5584 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
5585 OO
.ui
.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
5586 this.bindDocumentKeyDownListener
.apply( this, arguments
);
5590 * Unbind document key down listener.
5594 OO
.ui
.PopupWidget
.prototype.unbindDocumentKeyDownListener = function () {
5595 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5598 // Deprecated alias since 0.28.3
5599 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
5600 OO
.ui
.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
5601 this.unbindDocumentKeyDownListener
.apply( this, arguments
);
5605 * Show, hide, or toggle the visibility of the anchor.
5607 * @param {boolean} [show] Show anchor, omit to toggle
5609 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5610 show
= show
=== undefined ? !this.anchored
: !!show
;
5612 if ( this.anchored
!== show
) {
5614 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5615 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5617 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5618 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5620 this.anchored
= show
;
5625 * Change which edge the anchor appears on.
5627 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5629 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5630 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5631 throw new Error( 'Invalid value for edge: ' + edge
);
5633 if ( this.anchorEdge
!== null ) {
5634 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5636 this.anchorEdge
= edge
;
5637 if ( this.anchored
) {
5638 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5643 * Check if the anchor is visible.
5645 * @return {boolean} Anchor is visible
5647 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5648 return this.anchored
;
5652 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5653 * `.toggle( true )` after its #$element is attached to the DOM.
5655 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5656 * it in the right place and with the right dimensions only work correctly while it is attached.
5657 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5658 * strictly enforced, so currently it only generates a warning in the browser console.
5663 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5664 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5665 show
= show
=== undefined ? !this.isVisible() : !!show
;
5667 change
= show
!== this.isVisible();
5669 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5670 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5671 this.warnedUnattached
= true;
5673 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5674 // Fall back to the parent node if the floatableContainer is not set
5675 this.setFloatableContainer( this.$element
.parent() );
5678 if ( change
&& show
&& this.autoFlip
) {
5679 // Reset auto-flipping before showing the popup again. It's possible we no longer need to
5680 // flip (e.g. if the user scrolled).
5681 this.isAutoFlipped
= false;
5685 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5688 this.togglePositioning( show
&& !!this.$floatableContainer
);
5691 if ( this.autoClose
) {
5692 this.bindDocumentMouseDownListener();
5693 this.bindDocumentKeyDownListener();
5695 this.updateDimensions();
5696 this.toggleClipping( true );
5698 if ( this.autoFlip
) {
5699 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5700 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5701 // If opening the popup in the normal direction causes it to be clipped,
5702 // open in the opposite one instead
5703 normalHeight
= this.$element
.height();
5704 this.isAutoFlipped
= !this.isAutoFlipped
;
5706 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5707 // If that also causes it to be clipped, open in whichever direction
5708 // we have more space
5709 oppositeHeight
= this.$element
.height();
5710 if ( oppositeHeight
< normalHeight
) {
5711 this.isAutoFlipped
= !this.isAutoFlipped
;
5717 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5718 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5719 // If opening the popup in the normal direction causes it to be clipped,
5720 // open in the opposite one instead
5721 normalWidth
= this.$element
.width();
5722 this.isAutoFlipped
= !this.isAutoFlipped
;
5723 // Due to T180173 horizontally clipped PopupWidgets have messed up
5724 // dimensions, which causes positioning to be off. Toggle clipping back and
5725 // forth to work around.
5726 this.toggleClipping( false );
5728 this.toggleClipping( true );
5729 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5730 // If that also causes it to be clipped, open in whichever direction
5731 // we have more space
5732 oppositeWidth
= this.$element
.width();
5733 if ( oppositeWidth
< normalWidth
) {
5734 this.isAutoFlipped
= !this.isAutoFlipped
;
5735 // Due to T180173, horizontally clipped PopupWidgets have messed up
5736 // dimensions, which causes positioning to be off. Toggle clipping
5737 // back and forth to work around.
5738 this.toggleClipping( false );
5740 this.toggleClipping( true );
5747 this.emit( 'ready' );
5749 this.toggleClipping( false );
5750 if ( this.autoClose
) {
5751 this.unbindDocumentMouseDownListener();
5752 this.unbindDocumentKeyDownListener();
5761 * Set the size of the popup.
5763 * Changing the size may also change the popup's position depending on the alignment.
5765 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5766 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5767 * @param {boolean} [transition=false] Use a smooth transition
5770 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5771 this.width
= width
!== undefined ? width
: 320;
5772 this.height
= height
!== undefined ? height
: null;
5773 if ( this.isVisible() ) {
5774 this.updateDimensions( transition
);
5779 * Update the size and position.
5781 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5782 * be called automatically.
5784 * @param {boolean} [transition=false] Use a smooth transition
5787 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5790 // Prevent transition from being interrupted
5791 clearTimeout( this.transitionTimeout
);
5793 // Enable transition
5794 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5800 // Prevent transitioning after transition is complete
5801 this.transitionTimeout
= setTimeout( function () {
5802 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5805 // Prevent transitioning immediately
5806 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5813 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5814 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
,
5815 anchorPos
, anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
,
5816 floatablePos
, offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5818 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5819 popupPositionOppositeMap
= {
5827 'force-left': 'backwards',
5828 'force-right': 'forwards'
5831 'force-left': 'forwards',
5832 'force-right': 'backwards'
5844 backwards
: this.anchored
? 'before' : 'end'
5852 if ( !this.$container
) {
5853 // Lazy-initialize $container if not specified in constructor
5854 this.$container
= $( this.getClosestScrollableElementContainer() );
5856 direction
= this.$container
.css( 'direction' );
5858 // Set height and width before we do anything else, since it might cause our measurements
5859 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5861 width
: this.width
!== null ? this.width
: 'auto',
5862 height
: this.height
!== null ? this.height
: 'auto'
5865 align
= alignMap
[ direction
][ this.align
] || this.align
;
5866 popupPosition
= this.popupPosition
;
5867 if ( this.isAutoFlipped
) {
5868 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5871 // If the popup is positioned before or after, then the anchor positioning is vertical,
5872 // otherwise horizontal
5873 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5874 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5875 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5876 near
= vertical
? 'top' : 'left';
5877 far
= vertical
? 'bottom' : 'right';
5878 sizeProp
= vertical
? 'Height' : 'Width';
5879 popupSize
= vertical
?
5880 ( this.height
|| this.$popup
.height() ) :
5881 ( this.width
|| this.$popup
.width() );
5883 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5884 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5885 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5888 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5889 // Find out which property FloatableElement used for positioning, and adjust that value
5890 positionProp
= vertical
?
5891 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5892 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5894 // Figure out where the near and far edges of the popup and $floatableContainer are
5895 floatablePos
= this.$floatableContainer
.offset();
5896 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5897 // Measure where the offsetParent is and compute our position based on that and parentPosition
5898 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5899 { top
: 0, left
: 0 } :
5900 this.$element
.offsetParent().offset();
5902 if ( positionProp
=== near
) {
5903 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5904 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5906 popupPos
[ far
] = offsetParentPos
[ near
] +
5907 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5908 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5911 if ( this.anchored
) {
5912 // Position the anchor (which is positioned relative to the popup) to point to
5913 // $floatableContainer
5914 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5915 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5917 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more
5918 // space this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use
5919 // scrollWidth/Height
5920 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5921 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5922 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5923 // Not enough space for the anchor on the start side; pull the popup startwards
5924 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5925 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5926 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5927 // Not enough space for the anchor on the end side; pull the popup endwards
5928 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5929 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5931 positionAdjustment
= 0;
5934 positionAdjustment
= 0;
5937 // Check if the popup will go beyond the edge of this.$container
5938 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5939 { top
: 0, left
: 0 } :
5940 this.$container
.offset();
5941 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5942 if ( this.$container
[ 0 ] === document
.documentElement
) {
5943 viewportSpacing
= OO
.ui
.getViewportSpacing();
5944 containerPos
[ near
] += viewportSpacing
[ near
];
5945 containerPos
[ far
] -= viewportSpacing
[ far
];
5947 // Take into account how much the popup will move because of the adjustments we're going to make
5948 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5949 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5950 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5951 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5952 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5953 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5954 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5955 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5956 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5957 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5960 if ( this.anchored
) {
5961 // Adjust anchorOffset for positionAdjustment
5962 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5964 // Position the anchor
5965 anchorCss
[ start
] = anchorOffset
;
5966 this.$anchor
.css( anchorCss
);
5969 // Move the popup if needed
5970 parentPosition
[ positionProp
] += positionAdjustment
;
5972 return parentPosition
;
5976 * Set popup alignment
5978 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5979 * `backwards` or `forwards`.
5981 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5982 // Validate alignment
5983 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5986 this.align
= 'center';
5992 * Get popup alignment
5994 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5995 * `backwards` or `forwards`.
5997 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
6002 * Change the positioning of the popup.
6004 * @param {string} position 'above', 'below', 'before' or 'after'
6006 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
6007 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
6010 this.popupPosition
= position
;
6015 * Get popup positioning.
6017 * @return {string} 'above', 'below', 'before' or 'after'
6019 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
6020 return this.popupPosition
;
6024 * Set popup auto-flipping.
6026 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
6027 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
6028 * desired direction to display the popup without clipping
6030 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
6031 autoFlip
= !!autoFlip
;
6033 if ( this.autoFlip
!== autoFlip
) {
6034 this.autoFlip
= autoFlip
;
6039 * Set which elements will not close the popup when clicked.
6041 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
6043 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
6045 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
6046 this.$autoCloseIgnore
= $autoCloseIgnore
;
6050 * Get an ID of the body element, this can be used as the
6051 * `aria-describedby` attribute for an input field.
6053 * @return {string} The ID of the body element
6055 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
6056 var id
= this.$body
.attr( 'id' );
6057 if ( id
=== undefined ) {
6058 id
= OO
.ui
.generateElementId();
6059 this.$body
.attr( 'id', id
);
6065 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6066 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6067 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6068 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6074 * @param {Object} [config] Configuration options
6075 * @cfg {Object} [popup] Configuration to pass to popup
6076 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6078 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
6079 // Configuration initialization
6080 config
= config
|| {};
6083 this.popup
= new OO
.ui
.PopupWidget( $.extend(
6086 $floatableContainer
: this.$element
6090 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
6100 * @return {OO.ui.PopupWidget} Popup widget
6102 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
6107 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
6108 * which is used to display additional information or options.
6111 * // A PopupButtonWidget.
6112 * var popupButton = new OO.ui.PopupButtonWidget( {
6113 * label: 'Popup button with options',
6116 * $content: $( '<p>Additional options here.</p>' ),
6118 * align: 'force-left'
6121 * // Append the button to the DOM.
6122 * $( document.body ).append( popupButton.$element );
6125 * @extends OO.ui.ButtonWidget
6126 * @mixins OO.ui.mixin.PopupElement
6129 * @param {Object} [config] Configuration options
6130 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful
6131 * in cases where the expanded popup is larger than its containing `<div>`. The specified overlay
6132 * layer is usually on top of the containing `<div>` and has a larger area. By default, the popup
6133 * uses relative positioning.
6134 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
6136 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
6137 // Configuration initialization
6138 config
= config
|| {};
6140 // Parent constructor
6141 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
6143 // Mixin constructors
6144 OO
.ui
.mixin
.PopupElement
.call( this, config
);
6147 this.$overlay
= ( config
.$overlay
=== true ?
6148 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
6151 this.connect( this, {
6156 this.$element
.addClass( 'oo-ui-popupButtonWidget' );
6158 .addClass( 'oo-ui-popupButtonWidget-popup' )
6159 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6160 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6161 this.$overlay
.append( this.popup
.$element
);
6166 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
6167 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
6172 * Handle the button action being triggered.
6176 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
6177 this.popup
.toggle();
6181 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6183 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6188 * @mixins OO.ui.mixin.GroupElement
6191 * @param {Object} [config] Configuration options
6193 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
6194 // Mixin constructors
6195 OO
.ui
.mixin
.GroupElement
.call( this, config
);
6200 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
6205 * Set the disabled state of the widget.
6207 * This will also update the disabled state of child widgets.
6209 * @param {boolean} disabled Disable widget
6211 * @return {OO.ui.Widget} The widget, for chaining
6213 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
6217 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6218 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
6220 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6222 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6223 this.items
[ i
].updateDisabled();
6231 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6233 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group.
6234 * This allows bidirectional communication.
6236 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6244 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
6251 * Check if widget is disabled.
6253 * Checks parent if present, making disabled state inheritable.
6255 * @return {boolean} Widget is disabled
6257 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
6258 return this.disabled
||
6259 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
6263 * Set group element is in.
6265 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6267 * @return {OO.ui.Widget} The widget, for chaining
6269 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
6271 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6272 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
6274 // Initialize item disabled states
6275 this.updateDisabled();
6281 * OptionWidgets are special elements that can be selected and configured with data. The
6282 * data is often unique for each option, but it does not have to be. OptionWidgets are used
6283 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6284 * and examples, please see the [OOUI documentation on MediaWiki][1].
6286 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6289 * @extends OO.ui.Widget
6290 * @mixins OO.ui.mixin.ItemWidget
6291 * @mixins OO.ui.mixin.LabelElement
6292 * @mixins OO.ui.mixin.FlaggedElement
6293 * @mixins OO.ui.mixin.AccessKeyedElement
6294 * @mixins OO.ui.mixin.TitledElement
6297 * @param {Object} [config] Configuration options
6299 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
6300 // Configuration initialization
6301 config
= config
|| {};
6303 // Parent constructor
6304 OO
.ui
.OptionWidget
.parent
.call( this, config
);
6306 // Mixin constructors
6307 OO
.ui
.mixin
.ItemWidget
.call( this );
6308 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6309 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6310 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6311 OO
.ui
.mixin
.TitledElement
.call( this, config
);
6314 this.selected
= false;
6315 this.highlighted
= false;
6316 this.pressed
= false;
6320 .data( 'oo-ui-optionWidget', this )
6321 // Allow programmatic focussing (and by access key), but not tabbing
6322 .attr( 'tabindex', '-1' )
6323 .attr( 'role', 'option' )
6324 .attr( 'aria-selected', 'false' )
6325 .addClass( 'oo-ui-optionWidget' )
6326 .append( this.$label
);
6331 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6332 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6333 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6334 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6335 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6336 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.TitledElement
);
6338 /* Static Properties */
6341 * Whether this option can be selected. See #setSelected.
6345 * @property {boolean}
6347 OO
.ui
.OptionWidget
.static.selectable
= true;
6350 * Whether this option can be highlighted. See #setHighlighted.
6354 * @property {boolean}
6356 OO
.ui
.OptionWidget
.static.highlightable
= true;
6359 * Whether this option can be pressed. See #setPressed.
6363 * @property {boolean}
6365 OO
.ui
.OptionWidget
.static.pressable
= true;
6368 * Whether this option will be scrolled into view when it is selected.
6372 * @property {boolean}
6374 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6379 * Check if the option can be selected.
6381 * @return {boolean} Item is selectable
6383 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6384 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6388 * Check if the option can be highlighted. A highlight indicates that the option
6389 * may be selected when a user presses Enter key or clicks. Disabled items cannot
6392 * @return {boolean} Item is highlightable
6394 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6395 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6399 * Check if the option can be pressed. The pressed state occurs when a user mouses
6400 * down on an item, but has not yet let go of the mouse.
6402 * @return {boolean} Item is pressable
6404 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6405 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6409 * Check if the option is selected.
6411 * @return {boolean} Item is selected
6413 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6414 return this.selected
;
6418 * Check if the option is highlighted. A highlight indicates that the
6419 * item may be selected when a user presses Enter key or clicks.
6421 * @return {boolean} Item is highlighted
6423 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6424 return this.highlighted
;
6428 * Check if the option is pressed. The pressed state occurs when a user mouses
6429 * down on an item, but has not yet let go of the mouse. The item may appear
6430 * selected, but it will not be selected until the user releases the mouse.
6432 * @return {boolean} Item is pressed
6434 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6435 return this.pressed
;
6439 * Set the option’s selected state. In general, all modifications to the selection
6440 * should be handled by the SelectWidget’s
6441 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
6443 * @param {boolean} [state=false] Select option
6445 * @return {OO.ui.Widget} The widget, for chaining
6447 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6448 if ( this.constructor.static.selectable
) {
6449 this.selected
= !!state
;
6451 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6452 .attr( 'aria-selected', state
.toString() );
6453 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6454 this.scrollElementIntoView();
6456 this.updateThemeClasses();
6462 * Set the option’s highlighted state. In general, all programmatic
6463 * modifications to the highlight should be handled by the
6464 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6465 * method instead of this method.
6467 * @param {boolean} [state=false] Highlight option
6469 * @return {OO.ui.Widget} The widget, for chaining
6471 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6472 if ( this.constructor.static.highlightable
) {
6473 this.highlighted
= !!state
;
6474 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6475 this.updateThemeClasses();
6481 * Set the option’s pressed state. In general, all
6482 * programmatic modifications to the pressed state should be handled by the
6483 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6484 * method instead of this method.
6486 * @param {boolean} [state=false] Press option
6488 * @return {OO.ui.Widget} The widget, for chaining
6490 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6491 if ( this.constructor.static.pressable
) {
6492 this.pressed
= !!state
;
6493 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6494 this.updateThemeClasses();
6500 * Get text to match search strings against.
6502 * The default implementation returns the label text, but subclasses
6503 * can override this to provide more complex behavior.
6505 * @return {string|boolean} String to match search string against
6507 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6508 var label
= this.getLabel();
6509 return typeof label
=== 'string' ? label
: this.$label
.text();
6513 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6514 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6515 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6518 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For
6519 * more information, please see the [OOUI documentation on MediaWiki][1].
6522 * // A select widget with three options.
6523 * var select = new OO.ui.SelectWidget( {
6525 * new OO.ui.OptionWidget( {
6527 * label: 'Option One',
6529 * new OO.ui.OptionWidget( {
6531 * label: 'Option Two',
6533 * new OO.ui.OptionWidget( {
6535 * label: 'Option Three',
6539 * $( document.body ).append( select.$element );
6541 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6545 * @extends OO.ui.Widget
6546 * @mixins OO.ui.mixin.GroupWidget
6549 * @param {Object} [config] Configuration options
6550 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6551 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6552 * the [OOUI documentation on MediaWiki] [2] for examples.
6553 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6555 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6556 // Configuration initialization
6557 config
= config
|| {};
6559 // Parent constructor
6560 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6562 // Mixin constructors
6563 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, {
6564 $group
: this.$element
6568 this.pressed
= false;
6569 this.selecting
= null;
6570 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
6571 this.onDocumentMouseMoveHandler
= this.onDocumentMouseMove
.bind( this );
6572 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
6573 this.onDocumentKeyPressHandler
= this.onDocumentKeyPress
.bind( this );
6574 this.keyPressBuffer
= '';
6575 this.keyPressBufferTimer
= null;
6576 this.blockMouseOverEvents
= 0;
6579 this.connect( this, {
6583 focusin
: this.onFocus
.bind( this ),
6584 mousedown
: this.onMouseDown
.bind( this ),
6585 mouseover
: this.onMouseOver
.bind( this ),
6586 mouseleave
: this.onMouseLeave
.bind( this )
6591 // -depressed is a deprecated alias of -unpressed
6592 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-unpressed oo-ui-selectWidget-depressed' )
6593 .attr( 'role', 'listbox' );
6594 this.setFocusOwner( this.$element
);
6595 if ( Array
.isArray( config
.items
) ) {
6596 this.addItems( config
.items
);
6602 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6603 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6610 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6612 * @param {OO.ui.OptionWidget|null} item Highlighted item
6618 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6619 * pressed state of an option.
6621 * @param {OO.ui.OptionWidget|null} item Pressed item
6627 * A `select` event is emitted when the selection is modified programmatically with the #selectItem
6630 * @param {OO.ui.OptionWidget|null} item Selected item
6635 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6636 * @param {OO.ui.OptionWidget} item Chosen item
6642 * An `add` event is emitted when options are added to the select with the #addItems method.
6644 * @param {OO.ui.OptionWidget[]} items Added items
6645 * @param {number} index Index of insertion point
6651 * A `remove` event is emitted when options are removed from the select with the #clearItems
6652 * or #removeItems methods.
6654 * @param {OO.ui.OptionWidget[]} items Removed items
6657 /* Static methods */
6660 * Normalize text for filter matching
6662 * @param {string} text Text
6663 * @return {string} Normalized text
6665 OO
.ui
.SelectWidget
.static.normalizeForMatching = function ( text
) {
6666 // Replace trailing whitespace, normalize multiple spaces and make case insensitive
6667 var normalized
= text
.trim().replace( /\s+/, ' ' ).toLowerCase();
6669 // Normalize Unicode
6670 // eslint-disable-next-line no-restricted-properties
6671 if ( normalized
.normalize
) {
6672 // eslint-disable-next-line no-restricted-properties
6673 normalized
= normalized
.normalize();
6681 * Handle focus events
6684 * @param {jQuery.Event} event
6686 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6688 if ( event
.target
=== this.$element
[ 0 ] ) {
6689 // This widget was focussed, e.g. by the user tabbing to it.
6690 // The styles for focus state depend on one of the items being selected.
6691 if ( !this.findSelectedItem() ) {
6692 item
= this.findFirstSelectableItem();
6695 if ( event
.target
.tabIndex
=== -1 ) {
6696 // One of the options got focussed (and the event bubbled up here).
6697 // They can't be tabbed to, but they can be activated using access keys.
6698 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6699 item
= this.findTargetItem( event
);
6701 // There is something actually user-focusable in one of the labels of the options, and
6702 // the user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change
6709 if ( item
.constructor.static.highlightable
) {
6710 this.highlightItem( item
);
6712 this.selectItem( item
);
6716 if ( event
.target
!== this.$element
[ 0 ] ) {
6717 this.$focusOwner
.trigger( 'focus' );
6722 * Handle mouse down events.
6725 * @param {jQuery.Event} e Mouse down event
6726 * @return {undefined/boolean} False to prevent default if event is handled
6728 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6731 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6732 this.togglePressed( true );
6733 item
= this.findTargetItem( e
);
6734 if ( item
&& item
.isSelectable() ) {
6735 this.pressItem( item
);
6736 this.selecting
= item
;
6737 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6738 this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6745 * Handle document mouse up events.
6748 * @param {MouseEvent} e Mouse up event
6749 * @return {undefined/boolean} False to prevent default if event is handled
6751 OO
.ui
.SelectWidget
.prototype.onDocumentMouseUp = function ( e
) {
6754 this.togglePressed( false );
6755 if ( !this.selecting
) {
6756 item
= this.findTargetItem( e
);
6757 if ( item
&& item
.isSelectable() ) {
6758 this.selecting
= item
;
6761 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6762 this.pressItem( null );
6763 this.chooseItem( this.selecting
);
6764 this.selecting
= null;
6767 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6768 this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6773 // Deprecated alias since 0.28.3
6774 OO
.ui
.SelectWidget
.prototype.onMouseUp = function () {
6775 OO
.ui
.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
6776 this.onDocumentMouseUp
.apply( this, arguments
);
6780 * Handle document mouse move events.
6783 * @param {MouseEvent} e Mouse move event
6785 OO
.ui
.SelectWidget
.prototype.onDocumentMouseMove = function ( e
) {
6788 if ( !this.isDisabled() && this.pressed
) {
6789 item
= this.findTargetItem( e
);
6790 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6791 this.pressItem( item
);
6792 this.selecting
= item
;
6797 // Deprecated alias since 0.28.3
6798 OO
.ui
.SelectWidget
.prototype.onMouseMove = function () {
6799 OO
.ui
.warnDeprecation( 'onMouseMove is deprecated, use onDocumentMouseMove instead' );
6800 this.onDocumentMouseMove
.apply( this, arguments
);
6804 * Handle mouse over events.
6807 * @param {jQuery.Event} e Mouse over event
6808 * @return {undefined/boolean} False to prevent default if event is handled
6810 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6812 if ( this.blockMouseOverEvents
) {
6815 if ( !this.isDisabled() ) {
6816 item
= this.findTargetItem( e
);
6817 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6823 * Handle mouse leave events.
6826 * @param {jQuery.Event} e Mouse over event
6827 * @return {undefined/boolean} False to prevent default if event is handled
6829 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6830 if ( !this.isDisabled() ) {
6831 this.highlightItem( null );
6837 * Handle document key down events.
6840 * @param {KeyboardEvent} e Key down event
6842 OO
.ui
.SelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
6845 currentItem
= this.findHighlightedItem() || this.findSelectedItem();
6847 if ( !this.isDisabled() && this.isVisible() ) {
6848 switch ( e
.keyCode
) {
6849 case OO
.ui
.Keys
.ENTER
:
6850 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6851 // Was only highlighted, now let's select it. No-op if already selected.
6852 this.chooseItem( currentItem
);
6857 case OO
.ui
.Keys
.LEFT
:
6858 this.clearKeyPressBuffer();
6859 nextItem
= this.findRelativeSelectableItem( currentItem
, -1 );
6862 case OO
.ui
.Keys
.DOWN
:
6863 case OO
.ui
.Keys
.RIGHT
:
6864 this.clearKeyPressBuffer();
6865 nextItem
= this.findRelativeSelectableItem( currentItem
, 1 );
6868 case OO
.ui
.Keys
.ESCAPE
:
6869 case OO
.ui
.Keys
.TAB
:
6870 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6871 currentItem
.setHighlighted( false );
6873 this.unbindDocumentKeyDownListener();
6874 this.unbindDocumentKeyPressListener();
6875 // Don't prevent tabbing away / defocusing
6881 if ( nextItem
.constructor.static.highlightable
) {
6882 this.highlightItem( nextItem
);
6884 this.chooseItem( nextItem
);
6886 this.scrollItemIntoView( nextItem
);
6891 e
.stopPropagation();
6896 // Deprecated alias since 0.28.3
6897 OO
.ui
.SelectWidget
.prototype.onKeyDown = function () {
6898 OO
.ui
.warnDeprecation( 'onKeyDown is deprecated, use onDocumentKeyDown instead' );
6899 this.onDocumentKeyDown
.apply( this, arguments
);
6903 * Bind document key down listener.
6907 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyDownListener = function () {
6908 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6911 // Deprecated alias since 0.28.3
6912 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6913 OO
.ui
.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
6914 this.bindDocumentKeyDownListener
.apply( this, arguments
);
6918 * Unbind document key down listener.
6922 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
6923 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6926 // Deprecated alias since 0.28.3
6927 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6928 OO
.ui
.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
6929 this.unbindDocumentKeyDownListener
.apply( this, arguments
);
6933 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6935 * @param {OO.ui.OptionWidget} item Item to scroll into view
6937 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6939 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic
6940 // scrolling and around 100-150 ms after it is finished.
6941 this.blockMouseOverEvents
++;
6942 item
.scrollElementIntoView().done( function () {
6943 setTimeout( function () {
6944 widget
.blockMouseOverEvents
--;
6950 * Clear the key-press buffer
6954 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6955 if ( this.keyPressBufferTimer
) {
6956 clearTimeout( this.keyPressBufferTimer
);
6957 this.keyPressBufferTimer
= null;
6959 this.keyPressBuffer
= '';
6963 * Handle key press events.
6966 * @param {KeyboardEvent} e Key press event
6967 * @return {undefined/boolean} False to prevent default if event is handled
6969 OO
.ui
.SelectWidget
.prototype.onDocumentKeyPress = function ( e
) {
6970 var c
, filter
, item
;
6972 if ( !e
.charCode
) {
6973 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6974 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6979 // eslint-disable-next-line no-restricted-properties
6980 if ( String
.fromCodePoint
) {
6981 // eslint-disable-next-line no-restricted-properties
6982 c
= String
.fromCodePoint( e
.charCode
);
6984 c
= String
.fromCharCode( e
.charCode
);
6987 if ( this.keyPressBufferTimer
) {
6988 clearTimeout( this.keyPressBufferTimer
);
6990 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6992 item
= this.findHighlightedItem() || this.findSelectedItem();
6994 if ( this.keyPressBuffer
=== c
) {
6995 // Common (if weird) special case: typing "xxxx" will cycle through all
6996 // the items beginning with "x".
6998 item
= this.findRelativeSelectableItem( item
, 1 );
7001 this.keyPressBuffer
+= c
;
7004 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
7005 if ( !item
|| !filter( item
) ) {
7006 item
= this.findRelativeSelectableItem( item
, 1, filter
);
7009 if ( this.isVisible() && item
.constructor.static.highlightable
) {
7010 this.highlightItem( item
);
7012 this.chooseItem( item
);
7014 this.scrollItemIntoView( item
);
7018 e
.stopPropagation();
7021 // Deprecated alias since 0.28.3
7022 OO
.ui
.SelectWidget
.prototype.onKeyPress = function () {
7023 OO
.ui
.warnDeprecation( 'onKeyPress is deprecated, use onDocumentKeyPress instead' );
7024 this.onDocumentKeyPress
.apply( this, arguments
);
7028 * Get a matcher for the specific string
7031 * @param {string} query String to match against items
7032 * @param {string} [mode='prefix'] Matching mode: 'substring', 'prefix', or 'exact'
7033 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
7035 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( query
, mode
) {
7036 var normalizeForMatching
= this.constructor.static.normalizeForMatching
,
7037 normalizedQuery
= normalizeForMatching( query
);
7039 // Support deprecated exact=true argument
7040 if ( mode
=== true ) {
7044 return function ( item
) {
7045 var matchText
= normalizeForMatching( item
.getMatchText() );
7047 if ( normalizedQuery
=== '' ) {
7048 // Empty string matches all, except if we are in 'exact'
7049 // mode, where it doesn't match at all
7050 return mode
!== 'exact';
7055 return matchText
=== normalizedQuery
;
7057 return matchText
.indexOf( normalizedQuery
) !== -1;
7060 return matchText
.indexOf( normalizedQuery
) === 0;
7066 * Bind document key press listener.
7070 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7071 this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
7074 // Deprecated alias since 0.28.3
7075 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
7076 OO
.ui
.warnDeprecation( 'bindKeyPressListener is deprecated, use bindDocumentKeyPressListener instead' );
7077 this.bindDocumentKeyPressListener
.apply( this, arguments
);
7081 * Unbind document key down listener.
7083 * If you override this, be sure to call this.clearKeyPressBuffer() from your
7088 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7089 this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
7090 this.clearKeyPressBuffer();
7093 // Deprecated alias since 0.28.3
7094 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
7095 OO
.ui
.warnDeprecation( 'unbindKeyPressListener is deprecated, use unbindDocumentKeyPressListener instead' );
7096 this.unbindDocumentKeyPressListener
.apply( this, arguments
);
7100 * Visibility change handler
7103 * @param {boolean} visible
7105 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
7107 this.clearKeyPressBuffer();
7112 * Get the closest item to a jQuery.Event.
7115 * @param {jQuery.Event} e
7116 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
7118 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
7119 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
7120 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
7123 return $option
.data( 'oo-ui-optionWidget' ) || null;
7127 * Find selected item.
7129 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
7131 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
7134 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7135 if ( this.items
[ i
].isSelected() ) {
7136 return this.items
[ i
];
7143 * Find highlighted item.
7145 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
7147 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
7150 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7151 if ( this.items
[ i
].isHighlighted() ) {
7152 return this.items
[ i
];
7159 * Toggle pressed state.
7161 * Press is a state that occurs when a user mouses down on an item, but
7162 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
7163 * until the user releases the mouse.
7165 * @param {boolean} pressed An option is being pressed
7167 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
7168 if ( pressed
=== undefined ) {
7169 pressed
= !this.pressed
;
7171 if ( pressed
!== this.pressed
) {
7173 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
7174 // -depressed is a deprecated alias of -unpressed
7175 .toggleClass( 'oo-ui-selectWidget-unpressed oo-ui-selectWidget-depressed', !pressed
);
7176 this.pressed
= pressed
;
7181 * Highlight an option. If the `item` param is omitted, no options will be highlighted
7182 * and any existing highlight will be removed. The highlight is mutually exclusive.
7184 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
7187 * @return {OO.ui.Widget} The widget, for chaining
7189 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
7190 var i
, len
, highlighted
,
7193 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7194 highlighted
= this.items
[ i
] === item
;
7195 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
7196 this.items
[ i
].setHighlighted( highlighted
);
7202 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7204 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7206 this.emit( 'highlight', item
);
7213 * Fetch an item by its label.
7215 * @param {string} label Label of the item to select.
7216 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7217 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7219 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
7221 len
= this.items
.length
,
7222 filter
= this.getItemMatcher( label
, 'exact' );
7224 for ( i
= 0; i
< len
; i
++ ) {
7225 item
= this.items
[ i
];
7226 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7233 filter
= this.getItemMatcher( label
, 'prefix' );
7234 for ( i
= 0; i
< len
; i
++ ) {
7235 item
= this.items
[ i
];
7236 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7252 * Programmatically select an option by its label. If the item does not exist,
7253 * all options will be deselected.
7255 * @param {string} [label] Label of the item to select.
7256 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7259 * @return {OO.ui.Widget} The widget, for chaining
7261 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
7262 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
7263 if ( label
=== undefined || !itemFromLabel
) {
7264 return this.selectItem();
7266 return this.selectItem( itemFromLabel
);
7270 * Programmatically select an option by its data. If the `data` parameter is omitted,
7271 * or if the item does not exist, all options will be deselected.
7273 * @param {Object|string} [data] Value of the item to select, omit to deselect all
7276 * @return {OO.ui.Widget} The widget, for chaining
7278 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
7279 var itemFromData
= this.findItemFromData( data
);
7280 if ( data
=== undefined || !itemFromData
) {
7281 return this.selectItem();
7283 return this.selectItem( itemFromData
);
7287 * Programmatically select an option by its reference. If the `item` parameter is omitted,
7288 * all options will be deselected.
7290 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7293 * @return {OO.ui.Widget} The widget, for chaining
7295 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
7296 var i
, len
, selected
,
7299 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7300 selected
= this.items
[ i
] === item
;
7301 if ( this.items
[ i
].isSelected() !== selected
) {
7302 this.items
[ i
].setSelected( selected
);
7307 if ( item
&& !item
.constructor.static.highlightable
) {
7309 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7311 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7314 this.emit( 'select', item
);
7323 * Press is a state that occurs when a user mouses down on an item, but has not
7324 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7325 * releases the mouse.
7327 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7330 * @return {OO.ui.Widget} The widget, for chaining
7332 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
7333 var i
, len
, pressed
,
7336 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7337 pressed
= this.items
[ i
] === item
;
7338 if ( this.items
[ i
].isPressed() !== pressed
) {
7339 this.items
[ i
].setPressed( pressed
);
7344 this.emit( 'press', item
);
7353 * Note that ‘choose’ should never be modified programmatically. A user can choose
7354 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7355 * use the #selectItem method.
7357 * This method is identical to #selectItem, but may vary in subclasses that take additional action
7358 * when users choose an item with the keyboard or mouse.
7360 * @param {OO.ui.OptionWidget} item Item to choose
7363 * @return {OO.ui.Widget} The widget, for chaining
7365 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
7367 this.selectItem( item
);
7368 this.emit( 'choose', item
);
7375 * Find an option by its position relative to the specified item (or to the start of the option
7376 * array, if item is `null`). The direction in which to search through the option array is specified
7377 * with a number: -1 for reverse (the default) or 1 for forward. The method will return an option,
7378 * or `null` if there are no options in the array.
7380 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at
7381 * the beginning of the array.
7382 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7383 * @param {Function} [filter] Only consider items for which this function returns
7384 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
7385 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
7387 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
7388 var currentIndex
, nextIndex
, i
,
7389 increase
= direction
> 0 ? 1 : -1,
7390 len
= this.items
.length
;
7392 if ( item
instanceof OO
.ui
.OptionWidget
) {
7393 currentIndex
= this.items
.indexOf( item
);
7394 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7396 // If no item is selected and moving forward, start at the beginning.
7397 // If moving backward, start at the end.
7398 nextIndex
= direction
> 0 ? 0 : len
- 1;
7401 for ( i
= 0; i
< len
; i
++ ) {
7402 item
= this.items
[ nextIndex
];
7404 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7405 ( !filter
|| filter( item
) )
7409 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7415 * Find the next selectable item or `null` if there are no selectable items.
7416 * Disabled options and menu-section markers and breaks are not selectable.
7418 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7420 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7421 return this.findRelativeSelectableItem( null, 1 );
7425 * Add an array of options to the select. Optionally, an index number can be used to
7426 * specify an insertion point.
7428 * @param {OO.ui.OptionWidget[]} items Items to add
7429 * @param {number} [index] Index to insert items after
7432 * @return {OO.ui.Widget} The widget, for chaining
7434 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7436 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7438 // Always provide an index, even if it was omitted
7439 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7445 * Remove the specified array of options from the select. Options will be detached
7446 * from the DOM, not removed, so they can be reused later. To remove all options from
7447 * the select, you may wish to use the #clearItems method instead.
7449 * @param {OO.ui.OptionWidget[]} items Items to remove
7452 * @return {OO.ui.Widget} The widget, for chaining
7454 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7457 // Deselect items being removed
7458 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7460 if ( item
.isSelected() ) {
7461 this.selectItem( null );
7466 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7468 this.emit( 'remove', items
);
7474 * Clear all options from the select. Options will be detached from the DOM, not removed,
7475 * so that they can be reused later. To remove a subset of options from the select, use
7476 * the #removeItems method.
7480 * @return {OO.ui.Widget} The widget, for chaining
7482 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7483 var items
= this.items
.slice();
7486 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7489 this.selectItem( null );
7491 this.emit( 'remove', items
);
7497 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7499 * This is used to set `aria-activedescendant` and `aria-expanded` on it.
7502 * @param {jQuery} $focusOwner
7504 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7505 this.$focusOwner
= $focusOwner
;
7509 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7510 * with an {@link OO.ui.mixin.IconElement icon} and/or
7511 * {@link OO.ui.mixin.IndicatorElement indicator}.
7512 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7513 * options. For more information about options and selects, please see the
7514 * [OOUI documentation on MediaWiki][1].
7517 * // Decorated options in a select widget.
7518 * var select = new OO.ui.SelectWidget( {
7520 * new OO.ui.DecoratedOptionWidget( {
7522 * label: 'Option with icon',
7525 * new OO.ui.DecoratedOptionWidget( {
7527 * label: 'Option with indicator',
7532 * $( document.body ).append( select.$element );
7534 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7537 * @extends OO.ui.OptionWidget
7538 * @mixins OO.ui.mixin.IconElement
7539 * @mixins OO.ui.mixin.IndicatorElement
7542 * @param {Object} [config] Configuration options
7544 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7545 // Parent constructor
7546 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7548 // Mixin constructors
7549 OO
.ui
.mixin
.IconElement
.call( this, config
);
7550 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7554 .addClass( 'oo-ui-decoratedOptionWidget' )
7555 .prepend( this.$icon
)
7556 .append( this.$indicator
);
7561 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7562 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7563 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7566 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7567 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7568 * the [OOUI documentation on MediaWiki] [1] for more information.
7570 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7573 * @extends OO.ui.DecoratedOptionWidget
7576 * @param {Object} [config] Configuration options
7578 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7579 // Parent constructor
7580 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7583 this.checkIcon
= new OO
.ui
.IconWidget( {
7585 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7590 .prepend( this.checkIcon
.$element
)
7591 .addClass( 'oo-ui-menuOptionWidget' );
7596 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7598 /* Static Properties */
7604 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7607 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to
7608 * group one or more related {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets
7609 * cannot be highlighted or selected.
7612 * var dropdown = new OO.ui.DropdownWidget( {
7615 * new OO.ui.MenuSectionOptionWidget( {
7618 * new OO.ui.MenuOptionWidget( {
7620 * label: 'Welsh Corgi'
7622 * new OO.ui.MenuOptionWidget( {
7624 * label: 'Standard Poodle'
7626 * new OO.ui.MenuSectionOptionWidget( {
7629 * new OO.ui.MenuOptionWidget( {
7636 * $( document.body ).append( dropdown.$element );
7639 * @extends OO.ui.DecoratedOptionWidget
7642 * @param {Object} [config] Configuration options
7644 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7645 // Parent constructor
7646 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7650 .addClass( 'oo-ui-menuSectionOptionWidget' )
7651 .removeAttr( 'role aria-selected' );
7656 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7658 /* Static Properties */
7664 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7670 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7673 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7674 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7675 * See {@link OO.ui.DropdownWidget DropdownWidget},
7676 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}, and
7677 * {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7678 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7679 * and customized to be opened, closed, and displayed as needed.
7681 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7682 * mouse outside the menu.
7684 * Menus also have support for keyboard interaction:
7686 * - Enter/Return key: choose and select a menu option
7687 * - Up-arrow key: highlight the previous menu option
7688 * - Down-arrow key: highlight the next menu option
7689 * - Escape key: hide the menu
7691 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7693 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7694 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7697 * @extends OO.ui.SelectWidget
7698 * @mixins OO.ui.mixin.ClippableElement
7699 * @mixins OO.ui.mixin.FloatableElement
7702 * @param {Object} [config] Configuration options
7703 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu
7704 * items that match the text the user types. This config is used by
7705 * {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget} and
7706 * {@link OO.ui.mixin.LookupElement LookupElement}
7707 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7708 * the text the user types. This config is used by
7709 * {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
7710 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks
7711 * the mouse anywhere on the page outside of this widget, the menu is hidden. For example, if
7712 * there is a button that toggles the menu's visibility on click, the menu will be hidden then
7713 * re-shown when the user clicks that button, unless the button (or its parent widget) is passed
7715 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7716 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7717 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7718 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7719 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7720 * @cfg {string} [filterMode='prefix'] The mode by which the menu filters the results.
7721 * Options are 'exact', 'prefix' or 'substring'. See `OO.ui.SelectWidget#getItemMatcher`
7722 * @param {number|string} [width] Width of the menu as a number of pixels or CSS string with unit
7723 * suffix, used by {@link OO.ui.mixin.ClippableElement ClippableElement}
7725 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7726 // Configuration initialization
7727 config
= config
|| {};
7729 // Parent constructor
7730 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7732 // Mixin constructors
7733 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7734 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7736 // Initial vertical positions other than 'center' will result in
7737 // the menu being flipped if there is not enough space in the container.
7738 // Store the original position so we know what to reset to.
7739 this.originalVerticalPosition
= this.verticalPosition
;
7742 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7743 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7744 this.filterFromInput
= !!config
.filterFromInput
;
7745 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7746 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7747 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7748 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7749 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7750 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7751 this.lastHighlightedItem
= null;
7752 this.width
= config
.width
;
7753 this.filterMode
= config
.filterMode
;
7756 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7757 if ( config
.widget
) {
7758 this.setFocusOwner( config
.widget
.$tabIndexed
);
7761 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7762 // that reference properties not initialized at that time of parent class construction
7763 // TODO: Find a better way to handle post-constructor setup
7764 this.visible
= false;
7765 this.$element
.addClass( 'oo-ui-element-hidden' );
7766 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7771 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7772 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7773 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7780 * The menu is ready: it is visible and has been positioned and clipped.
7783 /* Static properties */
7786 * Positions to flip to if there isn't room in the container for the
7787 * menu in a specific direction.
7789 * @property {Object.<string,string>}
7791 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7801 * Handles document mouse down events.
7804 * @param {MouseEvent} e Mouse down event
7806 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7810 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7815 this.toggle( false );
7822 OO
.ui
.MenuSelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
7823 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7825 if ( !this.isDisabled() && this.isVisible() ) {
7826 switch ( e
.keyCode
) {
7827 case OO
.ui
.Keys
.LEFT
:
7828 case OO
.ui
.Keys
.RIGHT
:
7829 // Do nothing if a text field is associated, arrow keys will be handled natively
7830 if ( !this.$input
) {
7831 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7834 case OO
.ui
.Keys
.ESCAPE
:
7835 case OO
.ui
.Keys
.TAB
:
7836 if ( currentItem
) {
7837 currentItem
.setHighlighted( false );
7839 this.toggle( false );
7840 // Don't prevent tabbing away, prevent defocusing
7841 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7843 e
.stopPropagation();
7847 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7854 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7855 * or after items were added/removed (always).
7859 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7860 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7862 len
= this.items
.length
,
7863 showAll
= !this.isVisible(),
7866 if ( this.$input
&& this.filterFromInput
) {
7867 filter
= showAll
? null : this.getItemMatcher( this.$input
.val(), this.filterMode
);
7868 exactFilter
= this.getItemMatcher( this.$input
.val(), 'exact' );
7869 // Hide non-matching options, and also hide section headers if all options
7870 // in their section are hidden.
7871 for ( i
= 0; i
< len
; i
++ ) {
7872 item
= this.items
[ i
];
7873 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7875 // If the previous section was empty, hide its header
7876 section
.toggle( showAll
|| !sectionEmpty
);
7879 sectionEmpty
= true;
7880 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7881 visible
= showAll
|| filter( item
);
7882 exactMatch
= exactMatch
|| exactFilter( item
);
7883 anyVisible
= anyVisible
|| visible
;
7884 sectionEmpty
= sectionEmpty
&& !visible
;
7885 item
.toggle( visible
);
7888 // Process the final section
7890 section
.toggle( showAll
|| !sectionEmpty
);
7893 if ( anyVisible
&& this.items
.length
&& !exactMatch
) {
7894 this.scrollItemIntoView( this.items
[ 0 ] );
7897 if ( !anyVisible
) {
7898 this.highlightItem( null );
7901 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7904 this.highlightOnFilter
&&
7905 !( this.lastHighlightedItem
&& this.lastHighlightedItem
.isVisible() )
7907 // Highlight the first item on the list
7909 items
= this.getItems();
7910 for ( i
= 0; i
< items
.length
; i
++ ) {
7911 if ( items
[ i
].isVisible() ) {
7916 this.highlightItem( item
);
7917 this.lastHighlightedItem
= item
;
7922 // Reevaluate clipping
7929 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyDownListener = function () {
7930 if ( this.$input
) {
7931 this.$input
.on( 'keydown', this.onDocumentKeyDownHandler
);
7933 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyDownListener
.call( this );
7940 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
7941 if ( this.$input
) {
7942 this.$input
.off( 'keydown', this.onDocumentKeyDownHandler
);
7944 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyDownListener
.call( this );
7951 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7952 if ( this.$input
) {
7953 if ( this.filterFromInput
) {
7955 'keydown mouseup cut paste change input select',
7956 this.onInputEditHandler
7958 this.updateItemVisibility();
7961 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyPressListener
.call( this );
7968 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7969 if ( this.$input
) {
7970 if ( this.filterFromInput
) {
7972 'keydown mouseup cut paste change input select',
7973 this.onInputEditHandler
7975 this.updateItemVisibility();
7978 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyPressListener
.call( this );
7985 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is
7988 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with
7989 * the keyboard or mouse and it becomes selected. To select an item programmatically,
7990 * use the #selectItem method.
7992 * @param {OO.ui.OptionWidget} item Item to choose
7994 * @return {OO.ui.Widget} The widget, for chaining
7996 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7997 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7998 if ( this.hideOnChoose
) {
7999 this.toggle( false );
8007 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
8009 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
8011 this.updateItemVisibility();
8019 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
8021 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
8023 this.updateItemVisibility();
8031 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
8033 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
8035 this.updateItemVisibility();
8041 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
8042 * `.toggle( true )` after its #$element is attached to the DOM.
8044 * Do not show the menu while it is not attached to the DOM. The calculations required to display
8045 * it in the right place and with the right dimensions only work correctly while it is attached.
8046 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
8047 * strictly enforced, so currently it only generates a warning in the browser console.
8052 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
8053 var change
, originalHeight
, flippedHeight
;
8055 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
8056 change
= visible
!== this.isVisible();
8058 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
8059 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
8060 this.warnedUnattached
= true;
8063 if ( change
&& visible
) {
8064 // Reset position before showing the popup again. It's possible we no longer need to flip
8065 // (e.g. if the user scrolled).
8066 this.setVerticalPosition( this.originalVerticalPosition
);
8070 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
8076 this.setIdealSize( this.width
);
8077 } else if ( this.$floatableContainer
) {
8078 this.$clippable
.css( 'width', 'auto' );
8080 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
8081 // Dropdown is smaller than handle so expand to width
8082 this.$floatableContainer
[ 0 ].offsetWidth
:
8083 // Dropdown is larger than handle so auto size
8086 this.$clippable
.css( 'width', '' );
8089 this.togglePositioning( !!this.$floatableContainer
);
8090 this.toggleClipping( true );
8092 this.bindDocumentKeyDownListener();
8093 this.bindDocumentKeyPressListener();
8096 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
8097 this.originalVerticalPosition
!== 'center'
8099 // If opening the menu in one direction causes it to be clipped, flip it
8100 originalHeight
= this.$element
.height();
8101 this.setVerticalPosition(
8102 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
8104 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
8105 // If flipping also causes it to be clipped, open in whichever direction
8106 // we have more space
8107 flippedHeight
= this.$element
.height();
8108 if ( originalHeight
> flippedHeight
) {
8109 this.setVerticalPosition( this.originalVerticalPosition
);
8113 // Note that we do not flip the menu's opening direction if the clipping changes
8114 // later (e.g. after the user scrolls), that seems like it would be annoying
8116 this.$focusOwner
.attr( 'aria-expanded', 'true' );
8118 if ( this.findSelectedItem() ) {
8119 this.$focusOwner
.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() );
8120 this.findSelectedItem().scrollElementIntoView( { duration
: 0 } );
8124 if ( this.autoHide
) {
8125 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8128 this.emit( 'ready' );
8130 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
8131 this.unbindDocumentKeyDownListener();
8132 this.unbindDocumentKeyPressListener();
8133 this.$focusOwner
.attr( 'aria-expanded', 'false' );
8134 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
8135 this.togglePositioning( false );
8136 this.toggleClipping( false );
8144 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
8145 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
8146 * users can interact with it.
8148 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8149 * OO.ui.DropdownInputWidget instead.
8152 * // A DropdownWidget with a menu that contains three options.
8153 * var dropDown = new OO.ui.DropdownWidget( {
8154 * label: 'Dropdown menu: Select a menu option',
8157 * new OO.ui.MenuOptionWidget( {
8161 * new OO.ui.MenuOptionWidget( {
8165 * new OO.ui.MenuOptionWidget( {
8173 * $( document.body ).append( dropDown.$element );
8175 * dropDown.getMenu().selectItemByData( 'b' );
8177 * dropDown.getMenu().findSelectedItem().getData(); // Returns 'b'.
8179 * For more information, please see the [OOUI documentation on MediaWiki] [1].
8181 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8184 * @extends OO.ui.Widget
8185 * @mixins OO.ui.mixin.IconElement
8186 * @mixins OO.ui.mixin.IndicatorElement
8187 * @mixins OO.ui.mixin.LabelElement
8188 * @mixins OO.ui.mixin.TitledElement
8189 * @mixins OO.ui.mixin.TabIndexedElement
8192 * @param {Object} [config] Configuration options
8193 * @cfg {Object} [menu] Configuration options to pass to
8194 * {@link OO.ui.MenuSelectWidget menu select widget}.
8195 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
8196 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
8197 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
8198 * uses relative positioning.
8199 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
8201 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
8202 // Configuration initialization
8203 config
= $.extend( { indicator
: 'down' }, config
);
8205 // Parent constructor
8206 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
8208 // Properties (must be set before TabIndexedElement constructor call)
8209 this.$handle
= $( '<button>' );
8210 this.$overlay
= ( config
.$overlay
=== true ?
8211 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
8213 // Mixin constructors
8214 OO
.ui
.mixin
.IconElement
.call( this, config
);
8215 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8216 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8217 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, {
8218 $titled
: this.$label
8220 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, {
8221 $tabIndexed
: this.$handle
8225 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
8227 $floatableContainer
: this.$element
8232 click
: this.onClick
.bind( this ),
8233 keydown
: this.onKeyDown
.bind( this ),
8234 // Hack? Handle type-to-search when menu is not expanded and not handling its own events.
8235 keypress
: this.menu
.onDocumentKeyPressHandler
,
8236 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
8238 this.menu
.connect( this, {
8239 select
: 'onMenuSelect',
8240 toggle
: 'onMenuToggle'
8245 .addClass( 'oo-ui-dropdownWidget-handle' )
8248 'aria-owns': this.menu
.getElementId(),
8249 'aria-haspopup': 'listbox'
8251 .append( this.$icon
, this.$label
, this.$indicator
);
8253 .addClass( 'oo-ui-dropdownWidget' )
8254 .append( this.$handle
);
8255 this.$overlay
.append( this.menu
.$element
);
8260 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8261 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8262 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8263 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8264 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8265 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8272 * @return {OO.ui.MenuSelectWidget} Menu of widget
8274 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8279 * Handles menu select events.
8282 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8284 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8288 this.setLabel( null );
8292 selectedLabel
= item
.getLabel();
8294 // If the label is a DOM element, clone it, because setLabel will append() it
8295 if ( selectedLabel
instanceof $ ) {
8296 selectedLabel
= selectedLabel
.clone();
8299 this.setLabel( selectedLabel
);
8303 * Handle menu toggle events.
8306 * @param {boolean} isVisible Open state of the menu
8308 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8309 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8313 * Handle mouse click events.
8316 * @param {jQuery.Event} e Mouse click event
8317 * @return {undefined/boolean} False to prevent default if event is handled
8319 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8320 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8327 * Handle key down events.
8330 * @param {jQuery.Event} e Key down event
8331 * @return {undefined/boolean} False to prevent default if event is handled
8333 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8335 !this.isDisabled() &&
8337 e
.which
=== OO
.ui
.Keys
.ENTER
||
8339 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8340 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8341 // Space only closes the menu is the user is not typing to search.
8342 this.menu
.keyPressBuffer
=== ''
8345 !this.menu
.isVisible() &&
8347 e
.which
=== OO
.ui
.Keys
.UP
||
8348 e
.which
=== OO
.ui
.Keys
.DOWN
8359 * RadioOptionWidget is an option widget that looks like a radio button.
8360 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8361 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8363 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8366 * @extends OO.ui.OptionWidget
8369 * @param {Object} [config] Configuration options
8371 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8372 // Configuration initialization
8373 config
= config
|| {};
8375 // Properties (must be done before parent constructor which calls #setDisabled)
8376 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8378 // Parent constructor
8379 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8382 // Remove implicit role, we're handling it ourselves
8383 this.radio
.$input
.attr( 'role', 'presentation' );
8385 .addClass( 'oo-ui-radioOptionWidget' )
8386 .attr( 'role', 'radio' )
8387 .attr( 'aria-checked', 'false' )
8388 .removeAttr( 'aria-selected' )
8389 .prepend( this.radio
.$element
);
8394 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8396 /* Static Properties */
8402 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8408 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8414 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8420 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8427 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8428 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8430 this.radio
.setSelected( state
);
8432 .attr( 'aria-checked', state
.toString() )
8433 .removeAttr( 'aria-selected' );
8441 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8442 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8444 this.radio
.setDisabled( this.isDisabled() );
8450 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8451 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8452 * an interface for adding, removing and selecting options.
8453 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8455 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8456 * OO.ui.RadioSelectInputWidget instead.
8459 * // A RadioSelectWidget with RadioOptions.
8460 * var option1 = new OO.ui.RadioOptionWidget( {
8462 * label: 'Selected radio option'
8464 * option2 = new OO.ui.RadioOptionWidget( {
8466 * label: 'Unselected radio option'
8468 * radioSelect = new OO.ui.RadioSelectWidget( {
8469 * items: [ option1, option2 ]
8472 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8473 * radioSelect.selectItem( option1 );
8475 * $( document.body ).append( radioSelect.$element );
8477 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8481 * @extends OO.ui.SelectWidget
8482 * @mixins OO.ui.mixin.TabIndexedElement
8485 * @param {Object} [config] Configuration options
8487 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8488 // Parent constructor
8489 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8491 // Mixin constructors
8492 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8496 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8497 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8502 .addClass( 'oo-ui-radioSelectWidget' )
8503 .attr( 'role', 'radiogroup' );
8508 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8509 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8512 * MultioptionWidgets are special elements that can be selected and configured with data. The
8513 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8514 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8515 * and examples, please see the [OOUI documentation on MediaWiki][1].
8517 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8520 * @extends OO.ui.Widget
8521 * @mixins OO.ui.mixin.ItemWidget
8522 * @mixins OO.ui.mixin.LabelElement
8523 * @mixins OO.ui.mixin.TitledElement
8526 * @param {Object} [config] Configuration options
8527 * @cfg {boolean} [selected=false] Whether the option is initially selected
8529 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8530 // Configuration initialization
8531 config
= config
|| {};
8533 // Parent constructor
8534 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8536 // Mixin constructors
8537 OO
.ui
.mixin
.ItemWidget
.call( this );
8538 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8539 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8542 this.selected
= null;
8546 .addClass( 'oo-ui-multioptionWidget' )
8547 .append( this.$label
);
8548 this.setSelected( config
.selected
);
8553 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8554 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8555 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8556 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.TitledElement
);
8563 * A change event is emitted when the selected state of the option changes.
8565 * @param {boolean} selected Whether the option is now selected
8571 * Check if the option is selected.
8573 * @return {boolean} Item is selected
8575 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8576 return this.selected
;
8580 * Set the option’s selected state. In general, all modifications to the selection
8581 * should be handled by the SelectWidget’s
8582 * {@link OO.ui.SelectWidget#selectItem selectItem( [item] )} method instead of this method.
8584 * @param {boolean} [state=false] Select option
8586 * @return {OO.ui.Widget} The widget, for chaining
8588 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8590 if ( this.selected
!== state
) {
8591 this.selected
= state
;
8592 this.emit( 'change', state
);
8593 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8599 * MultiselectWidget allows selecting multiple options from a list.
8601 * For more information about menus and options, please see the [OOUI documentation
8604 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8608 * @extends OO.ui.Widget
8609 * @mixins OO.ui.mixin.GroupWidget
8610 * @mixins OO.ui.mixin.TitledElement
8613 * @param {Object} [config] Configuration options
8614 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8616 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8617 // Parent constructor
8618 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8620 // Configuration initialization
8621 config
= config
|| {};
8623 // Mixin constructors
8624 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8625 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8631 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8632 // by GroupElement only when items are added/removed
8633 this.connect( this, {
8634 select
: [ 'emit', 'change' ]
8638 if ( config
.items
) {
8639 this.addItems( config
.items
);
8641 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8642 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8643 .append( this.$group
);
8648 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8649 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8650 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.TitledElement
);
8657 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8663 * A select event is emitted when an item is selected or deselected.
8669 * Find options that are selected.
8671 * @return {OO.ui.MultioptionWidget[]} Selected options
8673 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8674 return this.items
.filter( function ( item
) {
8675 return item
.isSelected();
8680 * Find the data of options that are selected.
8682 * @return {Object[]|string[]} Values of selected options
8684 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8685 return this.findSelectedItems().map( function ( item
) {
8691 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8693 * @param {OO.ui.MultioptionWidget[]} items Items to select
8695 * @return {OO.ui.Widget} The widget, for chaining
8697 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8698 this.items
.forEach( function ( item
) {
8699 var selected
= items
.indexOf( item
) !== -1;
8700 item
.setSelected( selected
);
8706 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8708 * @param {Object[]|string[]} datas Values of items to select
8710 * @return {OO.ui.Widget} The widget, for chaining
8712 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8715 items
= datas
.map( function ( data
) {
8716 return widget
.findItemFromData( data
);
8718 this.selectItems( items
);
8723 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8724 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8725 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8727 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8730 * @extends OO.ui.MultioptionWidget
8733 * @param {Object} [config] Configuration options
8735 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8736 // Configuration initialization
8737 config
= config
|| {};
8739 // Properties (must be done before parent constructor which calls #setDisabled)
8740 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8742 // Parent constructor
8743 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8746 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8747 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8751 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8752 .prepend( this.checkbox
.$element
);
8757 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8759 /* Static Properties */
8765 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8770 * Handle checkbox selected state change.
8774 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8775 this.setSelected( this.checkbox
.isSelected() );
8781 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8782 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8783 this.checkbox
.setSelected( state
);
8790 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8791 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8792 this.checkbox
.setDisabled( this.isDisabled() );
8799 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8800 this.checkbox
.focus();
8804 * Handle key down events.
8807 * @param {jQuery.Event} e
8809 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8811 element
= this.getElementGroup(),
8814 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8815 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8816 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8817 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8827 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8828 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8829 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8830 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8832 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8833 * OO.ui.CheckboxMultiselectInputWidget instead.
8836 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8837 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8840 * label: 'Selected checkbox'
8842 * option2 = new OO.ui.CheckboxMultioptionWidget( {
8844 * label: 'Unselected checkbox'
8846 * multiselect = new OO.ui.CheckboxMultiselectWidget( {
8847 * items: [ option1, option2 ]
8849 * $( document.body ).append( multiselect.$element );
8851 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8854 * @extends OO.ui.MultiselectWidget
8857 * @param {Object} [config] Configuration options
8859 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8860 // Parent constructor
8861 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8864 this.$lastClicked
= null;
8867 this.$group
.on( 'click', this.onClick
.bind( this ) );
8870 this.$element
.addClass( 'oo-ui-checkboxMultiselectWidget' );
8875 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8880 * Get an option by its position relative to the specified item (or to the start of the
8881 * option array, if item is `null`). The direction in which to search through the option array
8882 * is specified with a number: -1 for reverse (the default) or 1 for forward. The method will
8883 * return an option, or `null` if there are no options in the array.
8885 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or
8886 * `null` to start at the beginning of the array.
8887 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8888 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items
8891 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8892 var currentIndex
, nextIndex
, i
,
8893 increase
= direction
> 0 ? 1 : -1,
8894 len
= this.items
.length
;
8897 currentIndex
= this.items
.indexOf( item
);
8898 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8900 // If no item is selected and moving forward, start at the beginning.
8901 // If moving backward, start at the end.
8902 nextIndex
= direction
> 0 ? 0 : len
- 1;
8905 for ( i
= 0; i
< len
; i
++ ) {
8906 item
= this.items
[ nextIndex
];
8907 if ( item
&& !item
.isDisabled() ) {
8910 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8916 * Handle click events on checkboxes.
8918 * @param {jQuery.Event} e
8920 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8921 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8922 $lastClicked
= this.$lastClicked
,
8923 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8924 .not( '.oo-ui-widget-disabled' );
8926 // Allow selecting multiple options at once by Shift-clicking them
8927 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8928 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8929 lastClickedIndex
= $options
.index( $lastClicked
);
8930 nowClickedIndex
= $options
.index( $nowClicked
);
8931 // If it's the same item, either the user is being silly, or it's a fake event generated
8932 // by the browser. In either case we don't need custom handling.
8933 if ( nowClickedIndex
!== lastClickedIndex
) {
8935 wasSelected
= items
[ nowClickedIndex
].isSelected();
8936 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8938 // This depends on the DOM order of the items and the order of the .items array being
8940 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8941 if ( !items
[ i
].isDisabled() ) {
8942 items
[ i
].setSelected( !wasSelected
);
8945 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8946 // handling first, then set our value. The order in which events happen is different for
8947 // clicks on the <input> and on the <label> and there are additional fake clicks fired
8948 // for non-click actions that change the checkboxes.
8950 setTimeout( function () {
8951 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8952 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8958 if ( $nowClicked
.length
) {
8959 this.$lastClicked
= $nowClicked
;
8967 * @return {OO.ui.Widget} The widget, for chaining
8969 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8971 if ( !this.isDisabled() ) {
8972 item
= this.getRelativeFocusableItem( null, 1 );
8983 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8988 * Progress bars visually display the status of an operation, such as a download,
8989 * and can be either determinate or indeterminate:
8991 * - **determinate** process bars show the percent of an operation that is complete.
8993 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8994 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8995 * not use percentages.
8997 * The value of the `progress` configuration determines whether the bar is determinate
9001 * // Examples of determinate and indeterminate progress bars.
9002 * var progressBar1 = new OO.ui.ProgressBarWidget( {
9005 * var progressBar2 = new OO.ui.ProgressBarWidget();
9007 * // Create a FieldsetLayout to layout progress bars.
9008 * var fieldset = new OO.ui.FieldsetLayout;
9009 * fieldset.addItems( [
9010 * new OO.ui.FieldLayout( progressBar1, {
9011 * label: 'Determinate',
9014 * new OO.ui.FieldLayout( progressBar2, {
9015 * label: 'Indeterminate',
9019 * $( document.body ).append( fieldset.$element );
9022 * @extends OO.ui.Widget
9025 * @param {Object} [config] Configuration options
9026 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
9027 * To create a determinate progress bar, specify a number that reflects the initial
9029 * By default, the progress bar is indeterminate.
9031 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
9032 // Configuration initialization
9033 config
= config
|| {};
9035 // Parent constructor
9036 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
9039 this.$bar
= $( '<div>' );
9040 this.progress
= null;
9043 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
9044 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
9047 role
: 'progressbar',
9049 'aria-valuemax': 100
9051 .addClass( 'oo-ui-progressBarWidget' )
9052 .append( this.$bar
);
9057 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
9059 /* Static Properties */
9065 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
9070 * Get the percent of the progress that has been completed. Indeterminate progresses will
9073 * @return {number|boolean} Progress percent
9075 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
9076 return this.progress
;
9080 * Set the percent of the process completed or `false` for an indeterminate process.
9082 * @param {number|boolean} progress Progress percent or `false` for indeterminate
9084 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
9085 this.progress
= progress
;
9087 if ( progress
!== false ) {
9088 this.$bar
.css( 'width', this.progress
+ '%' );
9089 this.$element
.attr( 'aria-valuenow', this.progress
);
9091 this.$bar
.css( 'width', '' );
9092 this.$element
.removeAttr( 'aria-valuenow' );
9094 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
9098 * InputWidget is the base class for all input widgets, which
9099 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox
9100 * inputs}, {@link OO.ui.RadioInputWidget radio inputs}, and
9101 * {@link OO.ui.ButtonInputWidget button inputs}.
9102 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
9104 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9108 * @extends OO.ui.Widget
9109 * @mixins OO.ui.mixin.FlaggedElement
9110 * @mixins OO.ui.mixin.TabIndexedElement
9111 * @mixins OO.ui.mixin.TitledElement
9112 * @mixins OO.ui.mixin.AccessKeyedElement
9115 * @param {Object} [config] Configuration options
9116 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9117 * @cfg {string} [value=''] The value of the input.
9118 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
9119 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
9120 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the
9121 * value of an input before it is accepted.
9123 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
9124 // Configuration initialization
9125 config
= config
|| {};
9127 // Parent constructor
9128 OO
.ui
.InputWidget
.parent
.call( this, config
);
9131 // See #reusePreInfuseDOM about config.$input
9132 this.$input
= config
.$input
|| this.getInputElement( config
);
9134 this.inputFilter
= config
.inputFilter
;
9136 // Mixin constructors
9137 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
9138 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, {
9139 $tabIndexed
: this.$input
9141 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, {
9142 $titled
: this.$input
9144 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, {
9145 $accessKeyed
: this.$input
9149 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
9153 .addClass( 'oo-ui-inputWidget-input' )
9154 .attr( 'name', config
.name
)
9155 .prop( 'disabled', this.isDisabled() );
9157 .addClass( 'oo-ui-inputWidget' )
9158 .append( this.$input
);
9159 this.setValue( config
.value
);
9161 this.setDir( config
.dir
);
9163 if ( config
.inputId
!== undefined ) {
9164 this.setInputId( config
.inputId
);
9170 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
9171 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
9172 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
9173 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
9174 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
9176 /* Static Methods */
9181 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9182 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9183 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
9184 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
9191 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9192 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9193 if ( config
.$input
&& config
.$input
.length
) {
9194 state
.value
= config
.$input
.val();
9195 // Might be better in TabIndexedElement, but it's awkward to do there because
9196 // mixins are awkward
9197 state
.focus
= config
.$input
.is( ':focus' );
9207 * A change event is emitted when the value of the input changes.
9209 * @param {string} value
9215 * Get input element.
9217 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9218 * different circumstances. The element must have a `value` property (like form elements).
9221 * @param {Object} config Configuration options
9222 * @return {jQuery} Input element
9224 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
9225 return $( '<input>' );
9229 * Handle potentially value-changing events.
9232 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9234 OO
.ui
.InputWidget
.prototype.onEdit = function () {
9236 if ( !this.isDisabled() ) {
9237 // Allow the stack to clear so the value will be updated
9238 setTimeout( function () {
9239 widget
.setValue( widget
.$input
.val() );
9245 * Get the value of the input.
9247 * @return {string} Input value
9249 OO
.ui
.InputWidget
.prototype.getValue = function () {
9250 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9251 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9252 var value
= this.$input
.val();
9253 if ( this.value
!== value
) {
9254 this.setValue( value
);
9260 * Set the directionality of the input.
9262 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9264 * @return {OO.ui.Widget} The widget, for chaining
9266 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9267 this.$input
.prop( 'dir', dir
);
9272 * Set the value of the input.
9274 * @param {string} value New value
9277 * @return {OO.ui.Widget} The widget, for chaining
9279 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9280 value
= this.cleanUpValue( value
);
9281 // Update the DOM if it has changed. Note that with cleanUpValue, it
9282 // is possible for the DOM value to change without this.value changing.
9283 if ( this.$input
.val() !== value
) {
9284 this.$input
.val( value
);
9286 if ( this.value
!== value
) {
9288 this.emit( 'change', this.value
);
9290 // The first time that the value is set (probably while constructing the widget),
9291 // remember it in defaultValue. This property can be later used to check whether
9292 // the value of the input has been changed since it was created.
9293 if ( this.defaultValue
=== undefined ) {
9294 this.defaultValue
= this.value
;
9295 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9301 * Clean up incoming value.
9303 * Ensures value is a string, and converts undefined and null to empty string.
9306 * @param {string} value Original value
9307 * @return {string} Cleaned up value
9309 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9310 if ( value
=== undefined || value
=== null ) {
9312 } else if ( this.inputFilter
) {
9313 return this.inputFilter( String( value
) );
9315 return String( value
);
9322 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9323 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9324 if ( this.$input
) {
9325 this.$input
.prop( 'disabled', this.isDisabled() );
9331 * Set the 'id' attribute of the `<input>` element.
9333 * @param {string} id
9335 * @return {OO.ui.Widget} The widget, for chaining
9337 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9338 this.$input
.attr( 'id', id
);
9345 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9346 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9347 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9348 this.setValue( state
.value
);
9350 if ( state
.focus
) {
9356 * Data widget intended for creating `<input type="hidden">` inputs.
9359 * @extends OO.ui.Widget
9362 * @param {Object} [config] Configuration options
9363 * @cfg {string} [value=''] The value of the input.
9364 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9366 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9367 // Configuration initialization
9368 config
= $.extend( { value
: '', name
: '' }, config
);
9370 // Parent constructor
9371 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9374 this.$element
.attr( {
9376 value
: config
.value
,
9379 this.$element
.removeAttr( 'aria-disabled' );
9384 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9386 /* Static Properties */
9392 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9395 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9396 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9397 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9398 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9399 * [OOUI documentation on MediaWiki] [1] for more information.
9402 * // A ButtonInputWidget rendered as an HTML button, the default.
9403 * var button = new OO.ui.ButtonInputWidget( {
9404 * label: 'Input button',
9408 * $( document.body ).append( button.$element );
9410 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9413 * @extends OO.ui.InputWidget
9414 * @mixins OO.ui.mixin.ButtonElement
9415 * @mixins OO.ui.mixin.IconElement
9416 * @mixins OO.ui.mixin.IndicatorElement
9417 * @mixins OO.ui.mixin.LabelElement
9420 * @param {Object} [config] Configuration options
9421 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute:
9422 * 'button', 'submit' or 'reset'.
9423 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9424 * Widgets configured to be an `<input>` do not support {@link #icon icons} and
9425 * {@link #indicator indicators},
9426 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should
9427 * only be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9429 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9430 // Configuration initialization
9431 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9433 // See InputWidget#reusePreInfuseDOM about config.$input
9434 if ( config
.$input
) {
9435 config
.$input
.empty();
9438 // Properties (must be set before parent constructor, which calls #setValue)
9439 this.useInputTag
= config
.useInputTag
;
9441 // Parent constructor
9442 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9444 // Mixin constructors
9445 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, {
9446 $button
: this.$input
9448 OO
.ui
.mixin
.IconElement
.call( this, config
);
9449 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9450 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9453 if ( !config
.useInputTag
) {
9454 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9456 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9461 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9462 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9463 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9464 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9465 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9467 /* Static Properties */
9473 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9481 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9483 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9484 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9490 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9492 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9493 * text, or `null` for no label
9495 * @return {OO.ui.Widget} The widget, for chaining
9497 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9498 if ( typeof label
=== 'function' ) {
9499 label
= OO
.ui
.resolveMsg( label
);
9502 if ( this.useInputTag
) {
9503 // Discard non-plaintext labels
9504 if ( typeof label
!== 'string' ) {
9508 this.$input
.val( label
);
9511 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9515 * Set the value of the input.
9517 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9518 * they do not support {@link #value values}.
9520 * @param {string} value New value
9522 * @return {OO.ui.Widget} The widget, for chaining
9524 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9525 if ( !this.useInputTag
) {
9526 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9534 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9535 // Disable generating `<label>` elements for buttons. One would very rarely need additional
9536 // label for a button, and it's already a big clickable target, and it causes
9537 // unexpected rendering.
9542 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9543 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9544 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9545 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9547 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9550 * // An example of selected, unselected, and disabled checkbox inputs.
9551 * var checkbox1 = new OO.ui.CheckboxInputWidget( {
9555 * checkbox2 = new OO.ui.CheckboxInputWidget( {
9558 * checkbox3 = new OO.ui.CheckboxInputWidget( {
9562 * // Create a fieldset layout with fields for each checkbox.
9563 * fieldset = new OO.ui.FieldsetLayout( {
9564 * label: 'Checkboxes'
9566 * fieldset.addItems( [
9567 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9568 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9569 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9571 * $( document.body ).append( fieldset.$element );
9573 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9576 * @extends OO.ui.InputWidget
9579 * @param {Object} [config] Configuration options
9580 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is
9583 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9584 // Configuration initialization
9585 config
= config
|| {};
9587 // Parent constructor
9588 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9591 this.checkIcon
= new OO
.ui
.IconWidget( {
9593 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9598 .addClass( 'oo-ui-checkboxInputWidget' )
9599 // Required for pretty styling in WikimediaUI theme
9600 .append( this.checkIcon
.$element
);
9601 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9606 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9608 /* Static Properties */
9614 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9616 /* Static Methods */
9621 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9622 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9623 state
.checked
= config
.$input
.prop( 'checked' );
9633 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9634 return $( '<input>' ).attr( 'type', 'checkbox' );
9640 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9642 if ( !this.isDisabled() ) {
9643 // Allow the stack to clear so the value will be updated
9644 setTimeout( function () {
9645 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9651 * Set selection state of this checkbox.
9653 * @param {boolean} state `true` for selected
9655 * @return {OO.ui.Widget} The widget, for chaining
9657 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9659 if ( this.selected
!== state
) {
9660 this.selected
= state
;
9661 this.$input
.prop( 'checked', this.selected
);
9662 this.emit( 'change', this.selected
);
9664 // The first time that the selection state is set (probably while constructing the widget),
9665 // remember it in defaultSelected. This property can be later used to check whether
9666 // the selection state of the input has been changed since it was created.
9667 if ( this.defaultSelected
=== undefined ) {
9668 this.defaultSelected
= this.selected
;
9669 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9675 * Check if this checkbox is selected.
9677 * @return {boolean} Checkbox is selected
9679 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9680 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9681 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9682 var selected
= this.$input
.prop( 'checked' );
9683 if ( this.selected
!== selected
) {
9684 this.setSelected( selected
);
9686 return this.selected
;
9692 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9693 if ( !this.isDisabled() ) {
9694 this.$handle
.trigger( 'click' );
9702 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9703 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9704 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9705 this.setSelected( state
.checked
);
9710 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9711 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the
9712 * value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9713 * more information about input widgets.
9715 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9716 * are no options. If no `value` configuration option is provided, the first option is selected.
9717 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9719 * This and OO.ui.RadioSelectInputWidget support similar configuration options.
9722 * // A DropdownInputWidget with three options.
9723 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9725 * { data: 'a', label: 'First' },
9726 * { data: 'b', label: 'Second', disabled: true },
9727 * { optgroup: 'Group label' },
9728 * { data: 'c', label: 'First sub-item)' }
9731 * $( document.body ).append( dropdownInput.$element );
9733 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9736 * @extends OO.ui.InputWidget
9739 * @param {Object} [config] Configuration options
9740 * @cfg {Object[]} [options=[]] Array of menu options in the format described above.
9741 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9742 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
9743 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
9744 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
9745 * uses relative positioning.
9746 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9748 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9749 // Configuration initialization
9750 config
= config
|| {};
9752 // Properties (must be done before parent constructor which calls #setDisabled)
9753 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9755 $overlay
: config
.$overlay
9759 // Set up the options before parent constructor, which uses them to validate config.value.
9760 // Use this instead of setOptions() because this.$input is not set up yet.
9761 this.setOptionsData( config
.options
|| [] );
9763 // Parent constructor
9764 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9767 this.dropdownWidget
.getMenu().connect( this, {
9768 select
: 'onMenuSelect'
9773 .addClass( 'oo-ui-dropdownInputWidget' )
9774 .append( this.dropdownWidget
.$element
);
9775 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9776 this.setTitledElement( this.dropdownWidget
.$handle
);
9781 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9789 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9790 return $( '<select>' );
9794 * Handles menu select events.
9797 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9799 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9800 this.setValue( item
? item
.getData() : '' );
9806 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9808 value
= this.cleanUpValue( value
);
9809 // Only allow setting values that are actually present in the dropdown
9810 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9811 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9812 this.dropdownWidget
.getMenu().selectItem( selected
);
9813 value
= selected
? selected
.getData() : '';
9814 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9815 if ( this.optionsDirty
) {
9816 // We reached this from the constructor or from #setOptions.
9817 // We have to update the <select> element.
9818 this.updateOptionsInterface();
9826 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9827 this.dropdownWidget
.setDisabled( state
);
9828 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9833 * Set the options available for this input.
9835 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9837 * @return {OO.ui.Widget} The widget, for chaining
9839 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9840 var value
= this.getValue();
9842 this.setOptionsData( options
);
9844 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9845 // In case the previous value is no longer an available option, select the first valid one.
9846 this.setValue( value
);
9852 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9854 * This method may be called before the parent constructor, so various properties may not be
9857 * @param {Object[]} options Array of menu options (see #constructor for details).
9860 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9861 var optionWidgets
, optIndex
, opt
, previousOptgroup
, optionWidget
, optValue
,
9864 this.optionsDirty
= true;
9866 // Go through all the supplied option configs and create either
9867 // MenuSectionOption or MenuOption widgets from each.
9869 for ( optIndex
= 0; optIndex
< options
.length
; optIndex
++ ) {
9870 opt
= options
[ optIndex
];
9872 if ( opt
.optgroup
!== undefined ) {
9873 // Create a <optgroup> menu item.
9874 optionWidget
= widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9875 previousOptgroup
= optionWidget
;
9878 // Create a normal <option> menu item.
9879 optValue
= widget
.cleanUpValue( opt
.data
);
9880 optionWidget
= widget
.createMenuOptionWidget(
9882 opt
.label
!== undefined ? opt
.label
: optValue
9886 // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
9888 opt
.disabled
!== undefined ||
9889 previousOptgroup
instanceof OO
.ui
.MenuSectionOptionWidget
&&
9890 previousOptgroup
.isDisabled()
9892 optionWidget
.setDisabled( true );
9895 optionWidgets
.push( optionWidget
);
9898 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9902 * Create a menu option widget.
9905 * @param {string} data Item data
9906 * @param {string} label Item label
9907 * @return {OO.ui.MenuOptionWidget} Option widget
9909 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9910 return new OO
.ui
.MenuOptionWidget( {
9917 * Create a menu section option widget.
9920 * @param {string} label Section item label
9921 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9923 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9924 return new OO
.ui
.MenuSectionOptionWidget( {
9930 * Update the user-visible interface to match the internal list of options and value.
9932 * This method must only be called after the parent constructor.
9936 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9938 $optionsContainer
= this.$input
,
9939 defaultValue
= this.defaultValue
,
9942 this.$input
.empty();
9944 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9947 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9948 $optionNode
= $( '<option>' )
9949 .attr( 'value', optionWidget
.getData() )
9950 .text( optionWidget
.getLabel() );
9952 // Remember original selection state. This property can be later used to check whether
9953 // the selection state of the input has been changed since it was created.
9954 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9956 $optionsContainer
.append( $optionNode
);
9958 $optionNode
= $( '<optgroup>' )
9959 .attr( 'label', optionWidget
.getLabel() );
9960 widget
.$input
.append( $optionNode
);
9961 $optionsContainer
= $optionNode
;
9964 // Disable the option or optgroup if required.
9965 if ( optionWidget
.isDisabled() ) {
9966 $optionNode
.prop( 'disabled', true );
9970 this.optionsDirty
= false;
9976 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9977 this.dropdownWidget
.focus();
9984 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9985 this.dropdownWidget
.blur();
9990 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9991 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9992 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9993 * please see the [OOUI documentation on MediaWiki][1].
9995 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9998 * // An example of selected, unselected, and disabled radio inputs
9999 * var radio1 = new OO.ui.RadioInputWidget( {
10003 * var radio2 = new OO.ui.RadioInputWidget( {
10006 * var radio3 = new OO.ui.RadioInputWidget( {
10010 * // Create a fieldset layout with fields for each radio button.
10011 * var fieldset = new OO.ui.FieldsetLayout( {
10012 * label: 'Radio inputs'
10014 * fieldset.addItems( [
10015 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
10016 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
10017 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
10019 * $( document.body ).append( fieldset.$element );
10021 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10024 * @extends OO.ui.InputWidget
10027 * @param {Object} [config] Configuration options
10028 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button
10031 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
10032 // Configuration initialization
10033 config
= config
|| {};
10035 // Parent constructor
10036 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
10040 .addClass( 'oo-ui-radioInputWidget' )
10041 // Required for pretty styling in WikimediaUI theme
10042 .append( $( '<span>' ) );
10043 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
10048 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
10050 /* Static Properties */
10056 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
10058 /* Static Methods */
10063 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10064 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10065 state
.checked
= config
.$input
.prop( 'checked' );
10075 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
10076 return $( '<input>' ).attr( 'type', 'radio' );
10082 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
10083 // RadioInputWidget doesn't track its state.
10087 * Set selection state of this radio button.
10089 * @param {boolean} state `true` for selected
10091 * @return {OO.ui.Widget} The widget, for chaining
10093 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
10094 // RadioInputWidget doesn't track its state.
10095 this.$input
.prop( 'checked', state
);
10096 // The first time that the selection state is set (probably while constructing the widget),
10097 // remember it in defaultSelected. This property can be later used to check whether
10098 // the selection state of the input has been changed since it was created.
10099 if ( this.defaultSelected
=== undefined ) {
10100 this.defaultSelected
= state
;
10101 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
10107 * Check if this radio button is selected.
10109 * @return {boolean} Radio is selected
10111 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
10112 return this.$input
.prop( 'checked' );
10118 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
10119 if ( !this.isDisabled() ) {
10120 this.$input
.trigger( 'click' );
10128 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
10129 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
10130 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
10131 this.setSelected( state
.checked
);
10136 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be
10137 * used within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with
10138 * the value of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
10139 * more information about input widgets.
10141 * This and OO.ui.DropdownInputWidget support similar configuration options.
10144 * // A RadioSelectInputWidget with three options
10145 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
10147 * { data: 'a', label: 'First' },
10148 * { data: 'b', label: 'Second'},
10149 * { data: 'c', label: 'Third' }
10152 * $( document.body ).append( radioSelectInput.$element );
10154 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10157 * @extends OO.ui.InputWidget
10160 * @param {Object} [config] Configuration options
10161 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10163 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
10164 // Configuration initialization
10165 config
= config
|| {};
10167 // Properties (must be done before parent constructor which calls #setDisabled)
10168 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
10169 // Set up the options before parent constructor, which uses them to validate config.value.
10170 // Use this instead of setOptions() because this.$input is not set up yet
10171 this.setOptionsData( config
.options
|| [] );
10173 // Parent constructor
10174 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
10177 this.radioSelectWidget
.connect( this, {
10178 select
: 'onMenuSelect'
10183 .addClass( 'oo-ui-radioSelectInputWidget' )
10184 .append( this.radioSelectWidget
.$element
);
10185 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
10190 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
10192 /* Static Methods */
10197 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10198 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10199 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
10206 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10207 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10208 // Cannot reuse the `<input type=radio>` set
10209 delete config
.$input
;
10219 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
10220 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
10221 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
10222 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
10226 * Handles menu select events.
10229 * @param {OO.ui.RadioOptionWidget} item Selected menu item
10231 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
10232 this.setValue( item
.getData() );
10238 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
10240 value
= this.cleanUpValue( value
);
10241 // Only allow setting values that are actually present in the dropdown
10242 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
10243 this.radioSelectWidget
.findFirstSelectableItem();
10244 this.radioSelectWidget
.selectItem( selected
);
10245 value
= selected
? selected
.getData() : '';
10246 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10253 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10254 this.radioSelectWidget
.setDisabled( state
);
10255 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10260 * Set the options available for this input.
10262 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10264 * @return {OO.ui.Widget} The widget, for chaining
10266 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10267 var value
= this.getValue();
10269 this.setOptionsData( options
);
10271 // Re-set the value to update the visible interface (RadioSelectWidget).
10272 // In case the previous value is no longer an available option, select the first valid one.
10273 this.setValue( value
);
10279 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10281 * This method may be called before the parent constructor, so various properties may not be
10284 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10287 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10290 this.radioSelectWidget
10292 .addItems( options
.map( function ( opt
) {
10293 var optValue
= widget
.cleanUpValue( opt
.data
);
10294 return new OO
.ui
.RadioOptionWidget( {
10296 label
: opt
.label
!== undefined ? opt
.label
: optValue
10304 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10305 this.radioSelectWidget
.focus();
10312 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10313 this.radioSelectWidget
.blur();
10318 * CheckboxMultiselectInputWidget is a
10319 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10320 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10321 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10322 * more information about input widgets.
10325 * // A CheckboxMultiselectInputWidget with three options.
10326 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10328 * { data: 'a', label: 'First' },
10329 * { data: 'b', label: 'Second' },
10330 * { data: 'c', label: 'Third' }
10333 * $( document.body ).append( multiselectInput.$element );
10335 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10338 * @extends OO.ui.InputWidget
10341 * @param {Object} [config] Configuration options
10342 * @cfg {Object[]} [options=[]] Array of menu options in the format
10343 * `{ data: …, label: …, disabled: … }`
10345 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10346 // Configuration initialization
10347 config
= config
|| {};
10349 // Properties (must be done before parent constructor which calls #setDisabled)
10350 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10351 // Must be set before the #setOptionsData call below
10352 this.inputName
= config
.name
;
10353 // Set up the options before parent constructor, which uses them to validate config.value.
10354 // Use this instead of setOptions() because this.$input is not set up yet
10355 this.setOptionsData( config
.options
|| [] );
10357 // Parent constructor
10358 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10361 this.checkboxMultiselectWidget
.connect( this, {
10362 select
: 'onCheckboxesSelect'
10367 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10368 .append( this.checkboxMultiselectWidget
.$element
);
10369 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10370 this.$input
.detach();
10375 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10377 /* Static Methods */
10382 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10383 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState(
10386 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10387 .toArray().map( function ( el
) { return el
.value
; } );
10394 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10395 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10396 // Cannot reuse the `<input type=checkbox>` set
10397 delete config
.$input
;
10407 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10409 return $( '<unused>' );
10413 * Handles CheckboxMultiselectWidget select events.
10417 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10418 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10424 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10425 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10426 .toArray().map( function ( el
) { return el
.value
; } );
10427 if ( this.value
!== value
) {
10428 this.setValue( value
);
10436 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10437 value
= this.cleanUpValue( value
);
10438 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10439 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10440 if ( this.optionsDirty
) {
10441 // We reached this from the constructor or from #setOptions.
10442 // We have to update the <select> element.
10443 this.updateOptionsInterface();
10449 * Clean up incoming value.
10451 * @param {string[]} value Original value
10452 * @return {string[]} Cleaned up value
10454 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10455 var i
, singleValue
,
10457 if ( !Array
.isArray( value
) ) {
10460 for ( i
= 0; i
< value
.length
; i
++ ) {
10461 singleValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10462 .call( this, value
[ i
] );
10463 // Remove options that we don't have here
10464 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10467 cleanValue
.push( singleValue
);
10475 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10476 this.checkboxMultiselectWidget
.setDisabled( state
);
10477 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10482 * Set the options available for this input.
10484 * @param {Object[]} options Array of menu options in the format
10485 * `{ data: …, label: …, disabled: … }`
10487 * @return {OO.ui.Widget} The widget, for chaining
10489 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10490 var value
= this.getValue();
10492 this.setOptionsData( options
);
10494 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10495 // This will also get rid of any stale options that we just removed.
10496 this.setValue( value
);
10502 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10504 * This method may be called before the parent constructor, so various properties may not be
10507 * @param {Object[]} options Array of menu options in the format
10508 * `{ data: …, label: … }`
10511 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10514 this.optionsDirty
= true;
10516 this.checkboxMultiselectWidget
10518 .addItems( options
.map( function ( opt
) {
10519 var optValue
, item
, optDisabled
;
10520 optValue
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
10521 .call( widget
, opt
.data
);
10522 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10523 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10525 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10526 disabled
: optDisabled
10528 // Set the 'name' and 'value' for form submission
10529 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10530 item
.checkbox
.setValue( optValue
);
10536 * Update the user-visible interface to match the internal list of options and value.
10538 * This method must only be called after the parent constructor.
10542 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10543 var defaultValue
= this.defaultValue
;
10545 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10546 // Remember original selection state. This property can be later used to check whether
10547 // the selection state of the input has been changed since it was created.
10548 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10549 item
.checkbox
.defaultSelected
= isDefault
;
10550 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10553 this.optionsDirty
= false;
10559 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10560 this.checkboxMultiselectWidget
.focus();
10565 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10566 * size of the field as well as its presentation. In addition, these widgets can be configured
10567 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an
10568 * optional validation-pattern (used to determine if an input value is valid or not) and an input
10569 * filter, which modifies incoming values rather than validating them.
10570 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10572 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10575 * // A TextInputWidget.
10576 * var textInput = new OO.ui.TextInputWidget( {
10577 * value: 'Text input'
10579 * $( document.body ).append( textInput.$element );
10581 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10584 * @extends OO.ui.InputWidget
10585 * @mixins OO.ui.mixin.IconElement
10586 * @mixins OO.ui.mixin.IndicatorElement
10587 * @mixins OO.ui.mixin.PendingElement
10588 * @mixins OO.ui.mixin.LabelElement
10591 * @param {Object} [config] Configuration options
10592 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10593 * 'email', 'url' or 'number'.
10594 * @cfg {string} [placeholder] Placeholder text
10595 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10596 * instruct the browser to focus this widget.
10597 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10598 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10600 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10601 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10602 * many emojis) count as 2 characters each.
10603 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10604 * the value or placeholder text: `'before'` or `'after'`
10605 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator:
10606 * 'required'`. Note that `false` & setting `indicator: 'required' will result in no indicator
10608 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10609 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined`
10610 * means leaving it up to the browser).
10611 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10612 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10613 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10614 * value for it to be considered valid; when Function, a function receiving the value as parameter
10615 * that must return true, or promise resolving to true, for it to be considered valid.
10617 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10618 // Configuration initialization
10619 config
= $.extend( {
10621 labelPosition
: 'after'
10624 // Parent constructor
10625 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10627 // Mixin constructors
10628 OO
.ui
.mixin
.IconElement
.call( this, config
);
10629 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10630 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
10631 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10634 this.type
= this.getSaneType( config
);
10635 this.readOnly
= false;
10636 this.required
= false;
10637 this.validate
= null;
10638 this.scrollWidth
= null;
10640 this.setValidation( config
.validate
);
10641 this.setLabelPosition( config
.labelPosition
);
10645 keypress
: this.onKeyPress
.bind( this ),
10646 blur
: this.onBlur
.bind( this ),
10647 focus
: this.onFocus
.bind( this )
10649 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10650 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10651 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10652 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10656 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10657 .append( this.$icon
, this.$indicator
);
10658 this.setReadOnly( !!config
.readOnly
);
10659 this.setRequired( !!config
.required
);
10660 if ( config
.placeholder
!== undefined ) {
10661 this.$input
.attr( 'placeholder', config
.placeholder
);
10663 if ( config
.maxLength
!== undefined ) {
10664 this.$input
.attr( 'maxlength', config
.maxLength
);
10666 if ( config
.autofocus
) {
10667 this.$input
.attr( 'autofocus', 'autofocus' );
10669 if ( config
.autocomplete
=== false ) {
10670 this.$input
.attr( 'autocomplete', 'off' );
10671 // Turning off autocompletion also disables "form caching" when the user navigates to a
10672 // different page and then clicks "Back". Re-enable it when leaving.
10673 // Borrowed from jQuery UI.
10675 beforeunload: function () {
10676 this.$input
.removeAttr( 'autocomplete' );
10678 pageshow: function () {
10679 // Browsers don't seem to actually fire this event on "Back", they instead just
10680 // reload the whole page... it shouldn't hurt, though.
10681 this.$input
.attr( 'autocomplete', 'off' );
10685 if ( config
.spellcheck
!== undefined ) {
10686 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10688 if ( this.label
) {
10689 this.isWaitingToBeAttached
= true;
10690 this.installParentChangeDetector();
10696 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10697 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10698 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10699 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10700 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10702 /* Static Properties */
10704 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10712 * An `enter` event is emitted when the user presses Enter key inside the text box.
10720 * Handle icon mouse down events.
10723 * @param {jQuery.Event} e Mouse down event
10724 * @return {undefined/boolean} False to prevent default if event is handled
10726 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10727 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10734 * Handle indicator mouse down events.
10737 * @param {jQuery.Event} e Mouse down event
10738 * @return {undefined/boolean} False to prevent default if event is handled
10740 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10741 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10748 * Handle key press events.
10751 * @param {jQuery.Event} e Key press event
10752 * @fires enter If Enter key is pressed
10754 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10755 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10756 this.emit( 'enter', e
);
10761 * Handle blur events.
10764 * @param {jQuery.Event} e Blur event
10766 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10767 this.setValidityFlag();
10771 * Handle focus events.
10774 * @param {jQuery.Event} e Focus event
10776 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10777 if ( this.isWaitingToBeAttached
) {
10778 // If we've received focus, then we must be attached to the document, and if
10779 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10780 this.onElementAttach();
10782 this.setValidityFlag( true );
10786 * Handle element attach events.
10789 * @param {jQuery.Event} e Element attach event
10791 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10792 this.isWaitingToBeAttached
= false;
10793 // Any previously calculated size is now probably invalid if we reattached elsewhere
10794 this.valCache
= null;
10795 this.positionLabel();
10799 * Handle debounced change events.
10801 * @param {string} value
10804 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10805 this.setValidityFlag();
10809 * Check if the input is {@link #readOnly read-only}.
10811 * @return {boolean}
10813 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10814 return this.readOnly
;
10818 * Set the {@link #readOnly read-only} state of the input.
10820 * @param {boolean} state Make input read-only
10822 * @return {OO.ui.Widget} The widget, for chaining
10824 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10825 this.readOnly
= !!state
;
10826 this.$input
.prop( 'readOnly', this.readOnly
);
10831 * Check if the input is {@link #required required}.
10833 * @return {boolean}
10835 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10836 return this.required
;
10840 * Set the {@link #required required} state of the input.
10842 * @param {boolean} state Make input required
10844 * @return {OO.ui.Widget} The widget, for chaining
10846 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10847 this.required
= !!state
;
10848 if ( this.required
) {
10850 .prop( 'required', true )
10851 .attr( 'aria-required', 'true' );
10852 if ( this.getIndicator() === null ) {
10853 this.setIndicator( 'required' );
10857 .prop( 'required', false )
10858 .removeAttr( 'aria-required' );
10859 if ( this.getIndicator() === 'required' ) {
10860 this.setIndicator( null );
10867 * Support function for making #onElementAttach work across browsers.
10869 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10870 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10872 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10873 * first time that the element gets attached to the documented.
10875 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10876 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10877 MutationObserver
= window
.MutationObserver
||
10878 window
.WebKitMutationObserver
||
10879 window
.MozMutationObserver
,
10882 if ( MutationObserver
) {
10883 // The new way. If only it wasn't so ugly.
10885 if ( this.isElementAttached() ) {
10886 // Widget is attached already, do nothing. This breaks the functionality of this
10887 // function when the widget is detached and reattached. Alas, doing this correctly with
10888 // MutationObserver would require observation of the whole document, which would hurt
10889 // performance of other, more important code.
10893 // Find topmost node in the tree
10894 topmostNode
= this.$element
[ 0 ];
10895 while ( topmostNode
.parentNode
) {
10896 topmostNode
= topmostNode
.parentNode
;
10899 // We have no way to detect the $element being attached somewhere without observing the
10900 // entire DOM with subtree modifications, which would hurt performance. So we cheat: we hook
10901 // to the parent node of $element, and instead detect when $element is removed from it (and
10902 // thus probably attached somewhere else). If there is no parent, we create a "fake" one. If
10903 // it doesn't get attached, we end up back here and create the parent.
10904 mutationObserver
= new MutationObserver( function ( mutations
) {
10905 var i
, j
, removedNodes
;
10906 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10907 removedNodes
= mutations
[ i
].removedNodes
;
10908 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10909 if ( removedNodes
[ j
] === topmostNode
) {
10910 setTimeout( onRemove
, 0 );
10917 onRemove = function () {
10918 // If the node was attached somewhere else, report it
10919 if ( widget
.isElementAttached() ) {
10920 widget
.onElementAttach();
10922 mutationObserver
.disconnect();
10923 widget
.installParentChangeDetector();
10926 // Create a fake parent and observe it
10927 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10928 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10930 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10931 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10932 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10940 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10941 if ( this.getSaneType( config
) === 'number' ) {
10942 return $( '<input>' )
10943 .attr( 'step', 'any' )
10944 .attr( 'type', 'number' );
10946 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10951 * Get sanitized value for 'type' for given config.
10953 * @param {Object} config Configuration options
10954 * @return {string|null}
10957 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10958 var allowedTypes
= [
10965 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10969 * Focus the input and select a specified range within the text.
10971 * @param {number} from Select from offset
10972 * @param {number} [to] Select to offset, defaults to from
10974 * @return {OO.ui.Widget} The widget, for chaining
10976 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10977 var isBackwards
, start
, end
,
10978 input
= this.$input
[ 0 ];
10982 isBackwards
= to
< from;
10983 start
= isBackwards
? to
: from;
10984 end
= isBackwards
? from : to
;
10989 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10991 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10992 // Rather than expensively check if the input is attached every time, just check
10993 // if it was the cause of an error being thrown. If not, rethrow the error.
10994 if ( this.getElementDocument().body
.contains( input
) ) {
11002 * Get an object describing the current selection range in a directional manner
11004 * @return {Object} Object containing 'from' and 'to' offsets
11006 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
11007 var input
= this.$input
[ 0 ],
11008 start
= input
.selectionStart
,
11009 end
= input
.selectionEnd
,
11010 isBackwards
= input
.selectionDirection
=== 'backward';
11013 from: isBackwards
? end
: start
,
11014 to
: isBackwards
? start
: end
11019 * Get the length of the text input value.
11021 * This could differ from the length of #getValue if the
11022 * value gets filtered
11024 * @return {number} Input length
11026 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
11027 return this.$input
[ 0 ].value
.length
;
11031 * Focus the input and select the entire text.
11034 * @return {OO.ui.Widget} The widget, for chaining
11036 OO
.ui
.TextInputWidget
.prototype.select = function () {
11037 return this.selectRange( 0, this.getInputLength() );
11041 * Focus the input and move the cursor to the start.
11044 * @return {OO.ui.Widget} The widget, for chaining
11046 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
11047 return this.selectRange( 0 );
11051 * Focus the input and move the cursor to the end.
11054 * @return {OO.ui.Widget} The widget, for chaining
11056 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
11057 return this.selectRange( this.getInputLength() );
11061 * Insert new content into the input.
11063 * @param {string} content Content to be inserted
11065 * @return {OO.ui.Widget} The widget, for chaining
11067 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
11069 range
= this.getRange(),
11070 value
= this.getValue();
11072 start
= Math
.min( range
.from, range
.to
);
11073 end
= Math
.max( range
.from, range
.to
);
11075 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
11076 this.selectRange( start
+ content
.length
);
11081 * Insert new content either side of a selection.
11083 * @param {string} pre Content to be inserted before the selection
11084 * @param {string} post Content to be inserted after the selection
11086 * @return {OO.ui.Widget} The widget, for chaining
11088 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
11090 range
= this.getRange(),
11091 offset
= pre
.length
;
11093 start
= Math
.min( range
.from, range
.to
);
11094 end
= Math
.max( range
.from, range
.to
);
11096 this.selectRange( start
).insertContent( pre
);
11097 this.selectRange( offset
+ end
).insertContent( post
);
11099 this.selectRange( offset
+ start
, offset
+ end
);
11104 * Set the validation pattern.
11106 * The validation pattern is either a regular expression, a function, or the symbolic name of a
11107 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11108 * value must contain only numbers).
11110 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
11111 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11113 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
11114 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
11115 this.validate
= validate
;
11117 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
11122 * Sets the 'invalid' flag appropriately.
11124 * @param {boolean} [isValid] Optionally override validation result
11126 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
11128 setFlag = function ( valid
) {
11130 widget
.$input
.attr( 'aria-invalid', 'true' );
11132 widget
.$input
.removeAttr( 'aria-invalid' );
11134 widget
.setFlags( { invalid
: !valid
} );
11137 if ( isValid
!== undefined ) {
11138 setFlag( isValid
);
11140 this.getValidity().then( function () {
11149 * Get the validity of current value.
11151 * This method returns a promise that resolves if the value is valid and rejects if
11152 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
11154 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
11156 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
11159 function rejectOrResolve( valid
) {
11161 return $.Deferred().resolve().promise();
11163 return $.Deferred().reject().promise();
11167 // Check browser validity and reject if it is invalid
11169 this.$input
[ 0 ].checkValidity
!== undefined &&
11170 this.$input
[ 0 ].checkValidity() === false
11172 return rejectOrResolve( false );
11175 // Run our checks if the browser thinks the field is valid
11176 if ( this.validate
instanceof Function
) {
11177 result
= this.validate( this.getValue() );
11178 if ( result
&& typeof result
.promise
=== 'function' ) {
11179 return result
.promise().then( function ( valid
) {
11180 return rejectOrResolve( valid
);
11183 return rejectOrResolve( result
);
11186 return rejectOrResolve( this.getValue().match( this.validate
) );
11191 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11193 * @param {string} labelPosition Label position, 'before' or 'after'
11195 * @return {OO.ui.Widget} The widget, for chaining
11197 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
11198 this.labelPosition
= labelPosition
;
11199 if ( this.label
) {
11200 // If there is no label and we only change the position, #updatePosition is a no-op,
11201 // but it takes really a lot of work to do nothing.
11202 this.updatePosition();
11208 * Update the position of the inline label.
11210 * This method is called by #setLabelPosition, and can also be called on its own if
11211 * something causes the label to be mispositioned.
11214 * @return {OO.ui.Widget} The widget, for chaining
11216 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
11217 var after
= this.labelPosition
=== 'after';
11220 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
11221 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
11223 this.valCache
= null;
11224 this.scrollWidth
= null;
11225 this.positionLabel();
11231 * Position the label by setting the correct padding on the input.
11235 * @return {OO.ui.Widget} The widget, for chaining
11237 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11238 var after
, rtl
, property
, newCss
;
11240 if ( this.isWaitingToBeAttached
) {
11241 // #onElementAttach will be called soon, which calls this method
11246 'padding-right': '',
11250 if ( this.label
) {
11251 this.$element
.append( this.$label
);
11253 this.$label
.detach();
11254 // Clear old values if present
11255 this.$input
.css( newCss
);
11259 after
= this.labelPosition
=== 'after';
11260 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11261 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11263 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11264 // We have to clear the padding on the other side, in case the element direction changed
11265 this.$input
.css( newCss
);
11271 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11272 * {@link OO.ui.mixin.IconElement search icon} by default.
11273 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11275 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11278 * @extends OO.ui.TextInputWidget
11281 * @param {Object} [config] Configuration options
11283 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11284 config
= $.extend( {
11288 // Parent constructor
11289 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11292 this.connect( this, {
11297 this.updateSearchIndicator();
11298 this.connect( this, {
11299 disable
: 'onDisable'
11305 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11313 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11320 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
11321 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11322 // Clear the text field
11323 this.setValue( '' );
11330 * Update the 'clear' indicator displayed on type: 'search' text
11331 * fields, hiding it when the field is already empty or when it's not
11334 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11335 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11336 this.setIndicator( null );
11338 this.setIndicator( 'clear' );
11343 * Handle change events.
11347 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11348 this.updateSearchIndicator();
11352 * Handle disable events.
11354 * @param {boolean} disabled Element is disabled
11357 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11358 this.updateSearchIndicator();
11364 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11365 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11366 this.updateSearchIndicator();
11371 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11372 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11373 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11374 * {@link OO.ui.mixin.IndicatorElement indicators}.
11375 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11377 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11380 * // A MultilineTextInputWidget.
11381 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11382 * value: 'Text input on multiple lines'
11384 * $( 'body' ).append( multilineTextInput.$element );
11386 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11389 * @extends OO.ui.TextInputWidget
11392 * @param {Object} [config] Configuration options
11393 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11394 * specifies minimum number of rows to display.
11395 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11396 * Use the #maxRows config to specify a maximum number of displayed rows.
11397 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11398 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11400 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11401 config
= $.extend( {
11404 // Parent constructor
11405 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11408 this.autosize
= !!config
.autosize
;
11409 this.styleHeight
= null;
11410 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11411 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11413 // Clone for resizing
11414 if ( this.autosize
) {
11415 this.$clone
= this.$input
11417 .removeAttr( 'id' )
11418 .removeAttr( 'name' )
11419 .insertAfter( this.$input
)
11420 .attr( 'aria-hidden', 'true' )
11421 .addClass( 'oo-ui-element-hidden' );
11425 this.connect( this, {
11430 if ( config
.rows
) {
11431 this.$input
.attr( 'rows', config
.rows
);
11433 if ( this.autosize
) {
11434 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11435 this.isWaitingToBeAttached
= true;
11436 this.installParentChangeDetector();
11442 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11444 /* Static Methods */
11449 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11450 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11451 state
.scrollTop
= config
.$input
.scrollTop();
11460 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11461 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11466 * Handle change events.
11470 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11477 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11478 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11485 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11487 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11489 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11490 // Some platforms emit keycode 10 for Control+Enter keypress in a textarea
11493 this.emit( 'enter', e
);
11498 * Automatically adjust the size of the text input.
11500 * This only affects multiline inputs that are {@link #autosize autosized}.
11503 * @return {OO.ui.Widget} The widget, for chaining
11506 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11507 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11508 idealHeight
, newHeight
, scrollWidth
, property
;
11510 if ( this.$input
.val() !== this.valCache
) {
11511 if ( this.autosize
) {
11513 .val( this.$input
.val() )
11514 .attr( 'rows', this.minRows
)
11515 // Set inline height property to 0 to measure scroll height
11516 .css( 'height', 0 );
11518 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11520 this.valCache
= this.$input
.val();
11522 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11524 // Remove inline height property to measure natural heights
11525 this.$clone
.css( 'height', '' );
11526 innerHeight
= this.$clone
.innerHeight();
11527 outerHeight
= this.$clone
.outerHeight();
11529 // Measure max rows height
11531 .attr( 'rows', this.maxRows
)
11532 .css( 'height', 'auto' )
11534 maxInnerHeight
= this.$clone
.innerHeight();
11536 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11537 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11538 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11539 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11541 this.$clone
.addClass( 'oo-ui-element-hidden' );
11543 // Only apply inline height when expansion beyond natural height is needed
11544 // Use the difference between the inner and outer height as a buffer
11545 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11546 if ( newHeight
!== this.styleHeight
) {
11547 this.$input
.css( 'height', newHeight
);
11548 this.styleHeight
= newHeight
;
11549 this.emit( 'resize' );
11552 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11553 if ( scrollWidth
!== this.scrollWidth
) {
11554 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11556 this.$label
.css( { right
: '', left
: '' } );
11557 this.$indicator
.css( { right
: '', left
: '' } );
11559 if ( scrollWidth
) {
11560 this.$indicator
.css( property
, scrollWidth
);
11561 if ( this.labelPosition
=== 'after' ) {
11562 this.$label
.css( property
, scrollWidth
);
11566 this.scrollWidth
= scrollWidth
;
11567 this.positionLabel();
11577 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11578 return $( '<textarea>' );
11582 * Check if the input automatically adjusts its size.
11584 * @return {boolean}
11586 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11587 return !!this.autosize
;
11593 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11594 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11595 if ( state
.scrollTop
!== undefined ) {
11596 this.$input
.scrollTop( state
.scrollTop
);
11601 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11602 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11603 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11605 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11606 * option, that option will appear to be selected.
11607 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11610 * After the user chooses an option, its `data` will be used as a new value for the widget.
11611 * A `label` also can be specified for each option: if given, it will be shown instead of the
11612 * `data` in the dropdown menu.
11614 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11616 * For more information about menus and options, please see the
11617 * [OOUI documentation on MediaWiki][1].
11620 * // A ComboBoxInputWidget.
11621 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11622 * value: 'Option 1',
11624 * { data: 'Option 1' },
11625 * { data: 'Option 2' },
11626 * { data: 'Option 3' }
11629 * $( document.body ).append( comboBox.$element );
11632 * // Example: A ComboBoxInputWidget with additional option labels.
11633 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11634 * value: 'Option 1',
11637 * data: 'Option 1',
11638 * label: 'Option One'
11641 * data: 'Option 2',
11642 * label: 'Option Two'
11645 * data: 'Option 3',
11646 * label: 'Option Three'
11650 * $( document.body ).append( comboBox.$element );
11652 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11655 * @extends OO.ui.TextInputWidget
11658 * @param {Object} [config] Configuration options
11659 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11660 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu
11662 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful
11663 * in cases where the expanded menu is larger than its containing `<div>`. The specified overlay
11664 * layer is usually on top of the containing `<div>` and has a larger area. By default, the menu
11665 * uses relative positioning.
11666 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11668 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11669 // Configuration initialization
11670 config
= $.extend( {
11671 autocomplete
: false
11674 // ComboBoxInputWidget shouldn't support `multiline`
11675 config
.multiline
= false;
11677 // See InputWidget#reusePreInfuseDOM about `config.$input`
11678 if ( config
.$input
) {
11679 config
.$input
.removeAttr( 'list' );
11682 // Parent constructor
11683 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11686 this.$overlay
= ( config
.$overlay
=== true ?
11687 OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11688 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11689 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11690 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11692 invisibleLabel
: true,
11693 disabled
: this.disabled
11695 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11699 $floatableContainer
: this.$element
,
11700 disabled
: this.isDisabled()
11706 this.connect( this, {
11707 change
: 'onInputChange',
11708 enter
: 'onInputEnter'
11710 this.dropdownButton
.connect( this, {
11711 click
: 'onDropdownButtonClick'
11713 this.menu
.connect( this, {
11714 choose
: 'onMenuChoose',
11715 add
: 'onMenuItemsChange',
11716 remove
: 'onMenuItemsChange',
11717 toggle
: 'onMenuToggle'
11721 this.$input
.attr( {
11723 'aria-owns': this.menu
.getElementId(),
11724 'aria-autocomplete': 'list'
11726 this.dropdownButton
.$button
.attr( {
11727 'aria-controls': this.menu
.getElementId()
11729 // Do not override options set via config.menu.items
11730 if ( config
.options
!== undefined ) {
11731 this.setOptions( config
.options
);
11733 this.$field
= $( '<div>' )
11734 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11735 .append( this.$input
, this.dropdownButton
.$element
);
11737 .addClass( 'oo-ui-comboBoxInputWidget' )
11738 .append( this.$field
);
11739 this.$overlay
.append( this.menu
.$element
);
11740 this.onMenuItemsChange();
11745 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11750 * Get the combobox's menu.
11752 * @return {OO.ui.MenuSelectWidget} Menu widget
11754 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11759 * Get the combobox's text input widget.
11761 * @return {OO.ui.TextInputWidget} Text input widget
11763 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11768 * Handle input change events.
11771 * @param {string} value New value
11773 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11774 var match
= this.menu
.findItemFromData( value
);
11776 this.menu
.selectItem( match
);
11777 if ( this.menu
.findHighlightedItem() ) {
11778 this.menu
.highlightItem( match
);
11781 if ( !this.isDisabled() ) {
11782 this.menu
.toggle( true );
11787 * Handle input enter events.
11791 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11792 if ( !this.isDisabled() ) {
11793 this.menu
.toggle( false );
11798 * Handle button click events.
11802 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11803 this.menu
.toggle();
11808 * Handle menu choose events.
11811 * @param {OO.ui.OptionWidget} item Chosen item
11813 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11814 this.setValue( item
.getData() );
11818 * Handle menu item change events.
11822 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11823 var match
= this.menu
.findItemFromData( this.getValue() );
11824 this.menu
.selectItem( match
);
11825 if ( this.menu
.findHighlightedItem() ) {
11826 this.menu
.highlightItem( match
);
11828 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11832 * Handle menu toggle events.
11835 * @param {boolean} isVisible Open state of the menu
11837 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11838 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11842 * Update the disabled state of the controls
11846 * @return {OO.ui.ComboBoxInputWidget} The widget, for chaining
11848 OO
.ui
.ComboBoxInputWidget
.prototype.updateControlsDisabled = function () {
11849 var disabled
= this.isDisabled() || this.isReadOnly();
11850 if ( this.dropdownButton
) {
11851 this.dropdownButton
.setDisabled( disabled
);
11854 this.menu
.setDisabled( disabled
);
11862 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function () {
11864 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.apply( this, arguments
);
11865 this.updateControlsDisabled();
11872 OO
.ui
.ComboBoxInputWidget
.prototype.setReadOnly = function () {
11874 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setReadOnly
.apply( this, arguments
);
11875 this.updateControlsDisabled();
11880 * Set the options available for this input.
11882 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11884 * @return {OO.ui.Widget} The widget, for chaining
11886 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11889 .addItems( options
.map( function ( opt
) {
11890 return new OO
.ui
.MenuOptionWidget( {
11892 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11900 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11901 * which is a widget that is specified by reference before any optional configuration settings.
11903 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of
11906 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11907 * A left-alignment is used for forms with many fields.
11908 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11909 * A right-alignment is used for long but familiar forms which users tab through,
11910 * verifying the current field with a quick glance at the label.
11911 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11912 * that users fill out from top to bottom.
11913 * - **inline**: The label is placed after the field-widget and aligned to the left.
11914 * An inline-alignment is best used with checkboxes or radio buttons.
11916 * Help text can either be:
11918 * - accessed via a help icon that appears in the upper right corner of the rendered field layout,
11920 * - shown as a subtle explanation below the label.
11922 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`.
11923 * If it is long or not essential, leave `helpInline` to its default, `false`.
11925 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11927 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11930 * @extends OO.ui.Layout
11931 * @mixins OO.ui.mixin.LabelElement
11932 * @mixins OO.ui.mixin.TitledElement
11935 * @param {OO.ui.Widget} fieldWidget Field widget
11936 * @param {Object} [config] Configuration options
11937 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11939 * @cfg {Array} [errors] Error messages about the widget, which will be
11940 * displayed below the widget.
11941 * @cfg {Array} [warnings] Warning messages about the widget, which will be
11942 * displayed below the widget.
11943 * The array may contain strings or OO.ui.HtmlSnippet instances.
11944 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11945 * below the widget.
11946 * The array may contain strings or OO.ui.HtmlSnippet instances.
11947 * These are more visible than `help` messages when `helpInline` is set, and so
11948 * might be good for transient messages.
11949 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11950 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11951 * corner of the rendered field; clicking it will display the text in a popup.
11952 * If `helpInline` is `true`, then a subtle description will be shown after the
11954 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11955 * or shown when the "help" icon is clicked.
11956 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11958 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11960 * @throws {Error} An error is thrown if no widget is specified
11962 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11963 // Allow passing positional parameters inside the config object
11964 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11965 config
= fieldWidget
;
11966 fieldWidget
= config
.fieldWidget
;
11969 // Make sure we have required constructor arguments
11970 if ( fieldWidget
=== undefined ) {
11971 throw new Error( 'Widget not found' );
11974 // Configuration initialization
11975 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11977 // Parent constructor
11978 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11980 // Mixin constructors
11981 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
11982 $label
: $( '<label>' )
11984 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
11987 this.fieldWidget
= fieldWidget
;
11989 this.warnings
= [];
11991 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11992 this.$messages
= $( '<ul>' );
11993 this.$header
= $( '<span>' );
11994 this.$body
= $( '<div>' );
11996 this.helpInline
= config
.helpInline
;
11999 this.fieldWidget
.connect( this, {
12000 disable
: 'onFieldDisable'
12004 this.$help
= config
.help
?
12005 this.createHelpElement( config
.help
, config
.$overlay
) :
12007 if ( this.fieldWidget
.getInputId() ) {
12008 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
12009 if ( this.helpInline
) {
12010 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
12013 this.$label
.on( 'click', function () {
12014 this.fieldWidget
.simulateLabelClick();
12016 if ( this.helpInline
) {
12017 this.$help
.on( 'click', function () {
12018 this.fieldWidget
.simulateLabelClick();
12023 .addClass( 'oo-ui-fieldLayout' )
12024 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
12025 .append( this.$body
);
12026 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
12027 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
12028 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
12030 .addClass( 'oo-ui-fieldLayout-field' )
12031 .append( this.fieldWidget
.$element
);
12033 this.setErrors( config
.errors
|| [] );
12034 this.setWarnings( config
.warnings
|| [] );
12035 this.setNotices( config
.notices
|| [] );
12036 this.setAlignment( config
.align
);
12037 // Call this again to take into account the widget's accessKey
12038 this.updateTitle();
12043 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
12044 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
12045 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
12050 * Handle field disable events.
12053 * @param {boolean} value Field is disabled
12055 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
12056 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
12060 * Get the widget contained by the field.
12062 * @return {OO.ui.Widget} Field widget
12064 OO
.ui
.FieldLayout
.prototype.getField = function () {
12065 return this.fieldWidget
;
12069 * Return `true` if the given field widget can be used with `'inline'` alignment (see
12070 * #setAlignment). Return `false` if it can't or if this can't be determined.
12072 * @return {boolean}
12074 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
12075 // This is very simplistic, but should be good enough.
12076 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
12081 * @param {string} kind 'error' or 'notice'
12082 * @param {string|OO.ui.HtmlSnippet} text
12085 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
12086 var $listItem
, $icon
, message
;
12087 $listItem
= $( '<li>' );
12088 if ( kind
=== 'error' ) {
12089 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'error' ] } ).$element
;
12090 $listItem
.attr( 'role', 'alert' );
12091 } else if ( kind
=== 'warning' ) {
12092 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
12093 $listItem
.attr( 'role', 'alert' );
12094 } else if ( kind
=== 'notice' ) {
12095 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
12099 message
= new OO
.ui
.LabelWidget( { label
: text
} );
12101 .append( $icon
, message
.$element
)
12102 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
12107 * Set the field alignment mode.
12110 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
12112 * @return {OO.ui.BookletLayout} The layout, for chaining
12114 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
12115 if ( value
!== this.align
) {
12116 // Default to 'left'
12117 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
12121 if ( value
=== 'inline' && !this.isFieldInline() ) {
12124 // Reorder elements
12126 if ( this.helpInline
) {
12127 if ( value
=== 'top' ) {
12128 this.$header
.append( this.$label
);
12129 this.$body
.append( this.$header
, this.$field
, this.$help
);
12130 } else if ( value
=== 'inline' ) {
12131 this.$header
.append( this.$label
, this.$help
);
12132 this.$body
.append( this.$field
, this.$header
);
12134 this.$header
.append( this.$label
, this.$help
);
12135 this.$body
.append( this.$header
, this.$field
);
12138 if ( value
=== 'top' ) {
12139 this.$header
.append( this.$help
, this.$label
);
12140 this.$body
.append( this.$header
, this.$field
);
12141 } else if ( value
=== 'inline' ) {
12142 this.$header
.append( this.$help
, this.$label
);
12143 this.$body
.append( this.$field
, this.$header
);
12145 this.$header
.append( this.$label
);
12146 this.$body
.append( this.$header
, this.$help
, this.$field
);
12149 // Set classes. The following classes can be used here:
12150 // * oo-ui-fieldLayout-align-left
12151 // * oo-ui-fieldLayout-align-right
12152 // * oo-ui-fieldLayout-align-top
12153 // * oo-ui-fieldLayout-align-inline
12154 if ( this.align
) {
12155 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
12157 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
12158 this.align
= value
;
12165 * Set the list of error messages.
12167 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
12168 * The array may contain strings or OO.ui.HtmlSnippet instances.
12170 * @return {OO.ui.BookletLayout} The layout, for chaining
12172 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
12173 this.errors
= errors
.slice();
12174 this.updateMessages();
12179 * Set the list of warning messages.
12181 * @param {Array} warnings Warning messages about the widget, which will be displayed below
12183 * The array may contain strings or OO.ui.HtmlSnippet instances.
12185 * @return {OO.ui.BookletLayout} The layout, for chaining
12187 OO
.ui
.FieldLayout
.prototype.setWarnings = function ( warnings
) {
12188 this.warnings
= warnings
.slice();
12189 this.updateMessages();
12194 * Set the list of notice messages.
12196 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
12197 * The array may contain strings or OO.ui.HtmlSnippet instances.
12199 * @return {OO.ui.BookletLayout} The layout, for chaining
12201 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
12202 this.notices
= notices
.slice();
12203 this.updateMessages();
12208 * Update the rendering of error, warning and notice messages.
12212 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
12214 this.$messages
.empty();
12216 if ( this.errors
.length
|| this.warnings
.length
|| this.notices
.length
) {
12217 this.$body
.after( this.$messages
);
12219 this.$messages
.remove();
12223 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
12224 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
12226 for ( i
= 0; i
< this.warnings
.length
; i
++ ) {
12227 this.$messages
.append( this.makeMessage( 'warning', this.warnings
[ i
] ) );
12229 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
12230 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
12235 * Include information about the widget's accessKey in our title. TitledElement calls this method.
12236 * (This is a bit of a hack.)
12239 * @param {string} title Tooltip label for 'title' attribute
12242 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
12243 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
12244 return this.fieldWidget
.formatTitleWithAccessKey( title
);
12250 * Creates and returns the help element. Also sets the `aria-describedby`
12251 * attribute on the main element of the `fieldWidget`.
12254 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
12255 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
12256 * @return {jQuery} The element that should become `this.$help`.
12258 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
12259 var helpId
, helpWidget
;
12261 if ( this.helpInline
) {
12262 helpWidget
= new OO
.ui
.LabelWidget( {
12264 classes
: [ 'oo-ui-inline-help' ]
12267 helpId
= helpWidget
.getElementId();
12269 helpWidget
= new OO
.ui
.PopupButtonWidget( {
12270 $overlay
: $overlay
,
12274 classes
: [ 'oo-ui-fieldLayout-help' ],
12277 label
: OO
.ui
.msg( 'ooui-field-help' ),
12278 invisibleLabel
: true
12280 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
12281 helpWidget
.getPopup().$body
.html( help
.toString() );
12283 helpWidget
.getPopup().$body
.text( help
);
12286 helpId
= helpWidget
.getPopup().getBodyId();
12289 // Set the 'aria-describedby' attribute on the fieldWidget
12290 // Preference given to an input or a button
12292 this.fieldWidget
.$input
||
12293 this.fieldWidget
.$button
||
12294 this.fieldWidget
.$element
12295 ).attr( 'aria-describedby', helpId
);
12297 return helpWidget
.$element
;
12301 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget,
12302 * a button, and an optional label and/or help text. The field-widget (e.g., a
12303 * {@link OO.ui.TextInputWidget TextInputWidget}), is required and is specified before any optional
12304 * configuration settings.
12306 * Labels can be aligned in one of four ways:
12308 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12309 * A left-alignment is used for forms with many fields.
12310 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12311 * A right-alignment is used for long but familiar forms which users tab through,
12312 * verifying the current field with a quick glance at the label.
12313 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12314 * that users fill out from top to bottom.
12315 * - **inline**: The label is placed after the field-widget and aligned to the left.
12316 * An inline-alignment is best used with checkboxes or radio buttons.
12318 * Help text is accessed via a help icon that appears in the upper right corner of the rendered
12319 * field layout when help text is specified.
12322 * // Example of an ActionFieldLayout
12323 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12324 * new OO.ui.TextInputWidget( {
12325 * placeholder: 'Field widget'
12327 * new OO.ui.ButtonWidget( {
12331 * label: 'An ActionFieldLayout. This label is aligned top',
12333 * help: 'This is help text'
12337 * $( document.body ).append( actionFieldLayout.$element );
12340 * @extends OO.ui.FieldLayout
12343 * @param {OO.ui.Widget} fieldWidget Field widget
12344 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12345 * @param {Object} config
12347 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12348 // Allow passing positional parameters inside the config object
12349 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12350 config
= fieldWidget
;
12351 fieldWidget
= config
.fieldWidget
;
12352 buttonWidget
= config
.buttonWidget
;
12355 // Parent constructor
12356 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12359 this.buttonWidget
= buttonWidget
;
12360 this.$button
= $( '<span>' );
12361 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12364 this.$element
.addClass( 'oo-ui-actionFieldLayout' );
12366 .addClass( 'oo-ui-actionFieldLayout-button' )
12367 .append( this.buttonWidget
.$element
);
12369 .addClass( 'oo-ui-actionFieldLayout-input' )
12370 .append( this.fieldWidget
.$element
);
12371 this.$field
.append( this.$input
, this.$button
);
12376 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12379 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12380 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12381 * configured with a label as well. For more information and examples,
12382 * please see the [OOUI documentation on MediaWiki][1].
12385 * // Example of a fieldset layout
12386 * var input1 = new OO.ui.TextInputWidget( {
12387 * placeholder: 'A text input field'
12390 * var input2 = new OO.ui.TextInputWidget( {
12391 * placeholder: 'A text input field'
12394 * var fieldset = new OO.ui.FieldsetLayout( {
12395 * label: 'Example of a fieldset layout'
12398 * fieldset.addItems( [
12399 * new OO.ui.FieldLayout( input1, {
12400 * label: 'Field One'
12402 * new OO.ui.FieldLayout( input2, {
12403 * label: 'Field Two'
12406 * $( document.body ).append( fieldset.$element );
12408 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12411 * @extends OO.ui.Layout
12412 * @mixins OO.ui.mixin.IconElement
12413 * @mixins OO.ui.mixin.LabelElement
12414 * @mixins OO.ui.mixin.GroupElement
12417 * @param {Object} [config] Configuration options
12418 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset.
12419 * See OO.ui.FieldLayout for more information about fields.
12420 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon
12421 * will appear in the upper-right corner of the rendered field; clicking it will display the text
12422 * in a popup. For important messages, you are advised to use `notices`, as they are always shown.
12423 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12424 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12426 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12427 // Configuration initialization
12428 config
= config
|| {};
12430 // Parent constructor
12431 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12433 // Mixin constructors
12434 OO
.ui
.mixin
.IconElement
.call( this, config
);
12435 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12436 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12439 this.$header
= $( '<legend>' );
12440 if ( config
.help
) {
12441 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
12442 $overlay
: config
.$overlay
,
12446 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12449 label
: OO
.ui
.msg( 'ooui-field-help' ),
12450 invisibleLabel
: true
12452 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12453 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
12455 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
12457 this.$help
= this.popupButtonWidget
.$element
;
12459 this.$help
= $( [] );
12464 .addClass( 'oo-ui-fieldsetLayout-header' )
12465 .append( this.$icon
, this.$label
, this.$help
);
12466 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12468 .addClass( 'oo-ui-fieldsetLayout' )
12469 .prepend( this.$header
, this.$group
);
12470 if ( Array
.isArray( config
.items
) ) {
12471 this.addItems( config
.items
);
12477 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12478 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12479 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12480 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12482 /* Static Properties */
12488 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12491 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use
12492 * browser-based form submission for the fields instead of handling them in JavaScript. Form layouts
12493 * can be configured with an HTML form action, an encoding type, and a method using the #action,
12494 * #enctype, and #method configs, respectively.
12495 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12497 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12498 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12499 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12500 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12501 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12502 * often have simplified APIs to match the capabilities of HTML forms.
12503 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12505 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12506 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12509 * // Example of a form layout that wraps a fieldset layout
12510 * var input1 = new OO.ui.TextInputWidget( {
12511 * placeholder: 'Username'
12513 * var input2 = new OO.ui.TextInputWidget( {
12514 * placeholder: 'Password',
12517 * var submit = new OO.ui.ButtonInputWidget( {
12521 * var fieldset = new OO.ui.FieldsetLayout( {
12522 * label: 'A form layout'
12524 * fieldset.addItems( [
12525 * new OO.ui.FieldLayout( input1, {
12526 * label: 'Username',
12529 * new OO.ui.FieldLayout( input2, {
12530 * label: 'Password',
12533 * new OO.ui.FieldLayout( submit )
12535 * var form = new OO.ui.FormLayout( {
12536 * items: [ fieldset ],
12537 * action: '/api/formhandler',
12540 * $( document.body ).append( form.$element );
12543 * @extends OO.ui.Layout
12544 * @mixins OO.ui.mixin.GroupElement
12547 * @param {Object} [config] Configuration options
12548 * @cfg {string} [method] HTML form `method` attribute
12549 * @cfg {string} [action] HTML form `action` attribute
12550 * @cfg {string} [enctype] HTML form `enctype` attribute
12551 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12553 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12556 // Configuration initialization
12557 config
= config
|| {};
12559 // Parent constructor
12560 OO
.ui
.FormLayout
.parent
.call( this, config
);
12562 // Mixin constructors
12563 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12566 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12568 // Make sure the action is safe
12569 action
= config
.action
;
12570 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12571 action
= './' + action
;
12576 .addClass( 'oo-ui-formLayout' )
12578 method
: config
.method
,
12580 enctype
: config
.enctype
12582 if ( Array
.isArray( config
.items
) ) {
12583 this.addItems( config
.items
);
12589 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12590 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12595 * A 'submit' event is emitted when the form is submitted.
12600 /* Static Properties */
12606 OO
.ui
.FormLayout
.static.tagName
= 'form';
12611 * Handle form submit events.
12614 * @param {jQuery.Event} e Submit event
12616 * @return {OO.ui.FormLayout} The layout, for chaining
12618 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12619 if ( this.emit( 'submit' ) ) {
12625 * PanelLayouts expand to cover the entire area of their parent. They can be configured with
12626 * scrolling, padding, and a frame, and are often used together with
12627 * {@link OO.ui.StackLayout StackLayouts}.
12630 * // Example of a panel layout
12631 * var panel = new OO.ui.PanelLayout( {
12635 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12637 * $( document.body ).append( panel.$element );
12640 * @extends OO.ui.Layout
12643 * @param {Object} [config] Configuration options
12644 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12645 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12646 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12647 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside
12650 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12651 // Configuration initialization
12652 config
= $.extend( {
12659 // Parent constructor
12660 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12663 this.$element
.addClass( 'oo-ui-panelLayout' );
12664 if ( config
.scrollable
) {
12665 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12667 if ( config
.padded
) {
12668 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12670 if ( config
.expanded
) {
12671 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12673 if ( config
.framed
) {
12674 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12680 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12685 * Focus the panel layout
12687 * The default implementation just focuses the first focusable element in the panel
12689 OO
.ui
.PanelLayout
.prototype.focus = function () {
12690 OO
.ui
.findFocusable( this.$element
).focus();
12694 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12695 * items), with small margins between them. Convenient when you need to put a number of block-level
12696 * widgets on a single line next to each other.
12698 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12701 * // HorizontalLayout with a text input and a label
12702 * var layout = new OO.ui.HorizontalLayout( {
12704 * new OO.ui.LabelWidget( { label: 'Label' } ),
12705 * new OO.ui.TextInputWidget( { value: 'Text' } )
12708 * $( document.body ).append( layout.$element );
12711 * @extends OO.ui.Layout
12712 * @mixins OO.ui.mixin.GroupElement
12715 * @param {Object} [config] Configuration options
12716 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12718 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12719 // Configuration initialization
12720 config
= config
|| {};
12722 // Parent constructor
12723 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12725 // Mixin constructors
12726 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12729 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12730 if ( Array
.isArray( config
.items
) ) {
12731 this.addItems( config
.items
);
12737 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12738 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12741 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12742 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12743 * (to adjust the value in increments) to allow the user to enter a number.
12746 * // A NumberInputWidget.
12747 * var numberInput = new OO.ui.NumberInputWidget( {
12748 * label: 'NumberInputWidget',
12749 * input: { value: 5 },
12753 * $( document.body ).append( numberInput.$element );
12756 * @extends OO.ui.TextInputWidget
12759 * @param {Object} [config] Configuration options
12760 * @cfg {Object} [minusButton] Configuration options to pass to the
12761 * {@link OO.ui.ButtonWidget decrementing button widget}.
12762 * @cfg {Object} [plusButton] Configuration options to pass to the
12763 * {@link OO.ui.ButtonWidget incrementing button widget}.
12764 * @cfg {number} [min=-Infinity] Minimum allowed value
12765 * @cfg {number} [max=Infinity] Maximum allowed value
12766 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12767 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or Up/Down arrow keys.
12768 * Defaults to `step` if specified, otherwise `1`.
12769 * @cfg {number} [pageStep=10*buttonStep] Delta when using the Page-up/Page-down keys.
12770 * Defaults to 10 times `buttonStep`.
12771 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12773 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12774 var $field
= $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' );
12776 // Configuration initialization
12777 config
= $.extend( {
12783 // For backward compatibility
12784 $.extend( config
, config
.input
);
12787 // Parent constructor
12788 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12792 if ( config
.showButtons
) {
12793 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12795 disabled
: this.isDisabled(),
12797 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12802 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12803 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12805 disabled
: this.isDisabled(),
12807 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12812 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12817 keydown
: this.onKeyDown
.bind( this ),
12818 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12820 if ( config
.showButtons
) {
12821 this.plusButton
.connect( this, {
12822 click
: [ 'onButtonClick', +1 ]
12824 this.minusButton
.connect( this, {
12825 click
: [ 'onButtonClick', -1 ]
12830 $field
.append( this.$input
);
12831 if ( config
.showButtons
) {
12833 .prepend( this.minusButton
.$element
)
12834 .append( this.plusButton
.$element
);
12838 if ( config
.allowInteger
|| config
.isInteger
) {
12839 // Backward compatibility
12842 this.setRange( config
.min
, config
.max
);
12843 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
12844 // Set the validation method after we set step and range
12845 // so that it doesn't immediately call setValidityFlag
12846 this.setValidation( this.validateNumber
.bind( this ) );
12849 .addClass( 'oo-ui-numberInputWidget' )
12850 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12856 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12860 // Backward compatibility
12861 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12862 this.setStep( flag
? 1 : null );
12864 // Backward compatibility
12865 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12867 // Backward compatibility
12868 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12869 return this.step
=== 1;
12871 // Backward compatibility
12872 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12875 * Set the range of allowed values
12877 * @param {number} min Minimum allowed value
12878 * @param {number} max Maximum allowed value
12880 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12882 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12886 this.$input
.attr( 'min', this.min
);
12887 this.$input
.attr( 'max', this.max
);
12888 this.setValidityFlag();
12892 * Get the current range
12894 * @return {number[]} Minimum and maximum values
12896 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12897 return [ this.min
, this.max
];
12901 * Set the stepping deltas
12903 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12904 * Defaults to `step` if specified, otherwise `1`.
12905 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12906 * Defaults to 10 times `buttonStep`.
12907 * @param {number|null} [step] If specified, the field only accepts values that are multiples
12910 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
12911 if ( buttonStep
=== undefined ) {
12912 buttonStep
= step
|| 1;
12914 if ( pageStep
=== undefined ) {
12915 pageStep
= 10 * buttonStep
;
12917 if ( step
!== null && step
<= 0 ) {
12918 throw new Error( 'Step value, if given, must be positive' );
12920 if ( buttonStep
<= 0 ) {
12921 throw new Error( 'Button step value must be positive' );
12923 if ( pageStep
<= 0 ) {
12924 throw new Error( 'Page step value must be positive' );
12927 this.buttonStep
= buttonStep
;
12928 this.pageStep
= pageStep
;
12929 this.$input
.attr( 'step', this.step
|| 'any' );
12930 this.setValidityFlag();
12936 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12937 if ( value
=== '' ) {
12938 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12939 // so here we make sure an 'empty' value is actually displayed as such.
12940 this.$input
.val( '' );
12942 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12946 * Get the current stepping values
12948 * @return {number[]} Button step, page step, and validity step
12950 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12951 return [ this.buttonStep
, this.pageStep
, this.step
];
12955 * Get the current value of the widget as a number
12957 * @return {number} May be NaN, or an invalid number
12959 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12960 return +this.getValue();
12964 * Adjust the value of the widget
12966 * @param {number} delta Adjustment amount
12968 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12969 var n
, v
= this.getNumericValue();
12972 if ( isNaN( delta
) || !isFinite( delta
) ) {
12973 throw new Error( 'Delta must be a finite number' );
12976 if ( isNaN( v
) ) {
12980 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12982 n
= Math
.round( n
/ this.step
) * this.step
;
12987 this.setValue( n
);
12994 * @param {string} value Field value
12995 * @return {boolean}
12997 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12999 if ( value
=== '' ) {
13000 return !this.isRequired();
13003 if ( isNaN( n
) || !isFinite( n
) ) {
13007 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
13011 if ( n
< this.min
|| n
> this.max
) {
13019 * Handle mouse click events.
13022 * @param {number} dir +1 or -1
13024 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
13025 this.adjustValue( dir
* this.buttonStep
);
13029 * Handle mouse wheel events.
13032 * @param {jQuery.Event} event
13033 * @return {undefined/boolean} False to prevent default if event is handled
13035 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
13038 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
13039 // Standard 'wheel' event
13040 if ( event
.originalEvent
.deltaMode
!== undefined ) {
13041 this.sawWheelEvent
= true;
13043 if ( event
.originalEvent
.deltaY
) {
13044 delta
= -event
.originalEvent
.deltaY
;
13045 } else if ( event
.originalEvent
.deltaX
) {
13046 delta
= event
.originalEvent
.deltaX
;
13049 // Non-standard events
13050 if ( !this.sawWheelEvent
) {
13051 if ( event
.originalEvent
.wheelDeltaX
) {
13052 delta
= -event
.originalEvent
.wheelDeltaX
;
13053 } else if ( event
.originalEvent
.wheelDeltaY
) {
13054 delta
= event
.originalEvent
.wheelDeltaY
;
13055 } else if ( event
.originalEvent
.wheelDelta
) {
13056 delta
= event
.originalEvent
.wheelDelta
;
13057 } else if ( event
.originalEvent
.detail
) {
13058 delta
= -event
.originalEvent
.detail
;
13063 delta
= delta
< 0 ? -1 : 1;
13064 this.adjustValue( delta
* this.buttonStep
);
13072 * Handle key down events.
13075 * @param {jQuery.Event} e Key down event
13076 * @return {undefined/boolean} False to prevent default if event is handled
13078 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
13079 if ( !this.isDisabled() ) {
13080 switch ( e
.which
) {
13081 case OO
.ui
.Keys
.UP
:
13082 this.adjustValue( this.buttonStep
);
13084 case OO
.ui
.Keys
.DOWN
:
13085 this.adjustValue( -this.buttonStep
);
13087 case OO
.ui
.Keys
.PAGEUP
:
13088 this.adjustValue( this.pageStep
);
13090 case OO
.ui
.Keys
.PAGEDOWN
:
13091 this.adjustValue( -this.pageStep
);
13100 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
13102 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
13104 if ( this.minusButton
) {
13105 this.minusButton
.setDisabled( this.isDisabled() );
13107 if ( this.plusButton
) {
13108 this.plusButton
.setDisabled( this.isDisabled() );
13116 //# sourceMappingURL=oojs-ui-core.js.map.json