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-01-23T01:14:20Z
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, otherwise only match descendants
215 * @return {boolean} The node is in the list of target nodes
217 OO
.ui
.contains = function ( containers
, contained
, matchContainers
) {
219 if ( !Array
.isArray( containers
) ) {
220 containers
= [ containers
];
222 for ( i
= containers
.length
- 1; i
>= 0; i
-- ) {
223 if ( ( matchContainers
&& contained
=== containers
[ i
] ) || $.contains( containers
[ i
], contained
) ) {
231 * Return a function, that, as long as it continues to be invoked, will not
232 * be triggered. The function will be called after it stops being called for
233 * N milliseconds. If `immediate` is passed, trigger the function on the
234 * leading edge, instead of the trailing.
236 * Ported from: http://underscorejs.org/underscore.js
238 * @param {Function} func Function to debounce
239 * @param {number} [wait=0] Wait period in milliseconds
240 * @param {boolean} [immediate] Trigger on leading edge
241 * @return {Function} Debounced function
243 OO
.ui
.debounce = function ( func
, wait
, immediate
) {
248 later = function () {
251 func
.apply( context
, args
);
254 if ( immediate
&& !timeout
) {
255 func
.apply( context
, args
);
257 if ( !timeout
|| wait
) {
258 clearTimeout( timeout
);
259 timeout
= setTimeout( later
, wait
);
265 * Puts a console warning with provided message.
267 * @param {string} message Message
269 OO
.ui
.warnDeprecation = function ( message
) {
270 if ( OO
.getProp( window
, 'console', 'warn' ) !== undefined ) {
271 // eslint-disable-next-line no-console
272 console
.warn( message
);
277 * Returns a function, that, when invoked, will only be triggered at most once
278 * during a given window of time. If called again during that window, it will
279 * wait until the window ends and then trigger itself again.
281 * As it's not knowable to the caller whether the function will actually run
282 * when the wrapper is called, return values from the function are entirely
285 * @param {Function} func Function to throttle
286 * @param {number} wait Throttle window length, in milliseconds
287 * @return {Function} Throttled function
289 OO
.ui
.throttle = function ( func
, wait
) {
290 var context
, args
, timeout
,
294 previous
= OO
.ui
.now();
295 func
.apply( context
, args
);
298 // Check how long it's been since the last time the function was
299 // called, and whether it's more or less than the requested throttle
300 // period. If it's less, run the function immediately. If it's more,
301 // set a timeout for the remaining time -- but don't replace an
302 // existing timeout, since that'd indefinitely prolong the wait.
303 var remaining
= wait
- ( OO
.ui
.now() - previous
);
306 if ( remaining
<= 0 ) {
307 // Note: unless wait was ridiculously large, this means we'll
308 // automatically run the first time the function was called in a
309 // given period. (If you provide a wait period larger than the
310 // current Unix timestamp, you *deserve* unexpected behavior.)
311 clearTimeout( timeout
);
313 } else if ( !timeout
) {
314 timeout
= setTimeout( run
, remaining
);
320 * A (possibly faster) way to get the current timestamp as an integer
322 * @return {number} Current timestamp, in milliseconds since the Unix epoch
324 OO
.ui
.now
= Date
.now
|| function () {
325 return new Date().getTime();
329 * Reconstitute a JavaScript object corresponding to a widget created by
330 * the PHP implementation.
332 * This is an alias for `OO.ui.Element.static.infuse()`.
334 * @param {string|HTMLElement|jQuery} idOrNode
335 * A DOM id (if a string) or node for the widget to infuse.
336 * @param {Object} [config] Configuration options
337 * @return {OO.ui.Element}
338 * The `OO.ui.Element` corresponding to this (infusable) document node.
340 OO
.ui
.infuse = function ( idOrNode
, config
) {
341 return OO
.ui
.Element
.static.infuse( idOrNode
, config
);
346 * Message store for the default implementation of OO.ui.msg
348 * Environments that provide a localization system should not use this, but should override
349 * OO.ui.msg altogether.
354 // Tool tip for a button that moves items in a list down one place
355 'ooui-outline-control-move-down': 'Move item down',
356 // Tool tip for a button that moves items in a list up one place
357 'ooui-outline-control-move-up': 'Move item up',
358 // Tool tip for a button that removes items from a list
359 'ooui-outline-control-remove': 'Remove item',
360 // Label for the toolbar group that contains a list of all other available tools
361 'ooui-toolbar-more': 'More',
362 // Label for the fake tool that expands the full list of tools in a toolbar group
363 'ooui-toolgroup-expand': 'More',
364 // Label for the fake tool that collapses the full list of tools in a toolbar group
365 'ooui-toolgroup-collapse': 'Fewer',
366 // Default label for the tooltip for the button that removes a tag item
367 'ooui-item-remove': 'Remove',
368 // Default label for the accept button of a confirmation dialog
369 'ooui-dialog-message-accept': 'OK',
370 // Default label for the reject button of a confirmation dialog
371 'ooui-dialog-message-reject': 'Cancel',
372 // Title for process dialog error description
373 'ooui-dialog-process-error': 'Something went wrong',
374 // Label for process dialog dismiss error button, visible when describing errors
375 'ooui-dialog-process-dismiss': 'Dismiss',
376 // Label for process dialog retry action button, visible when describing only recoverable errors
377 'ooui-dialog-process-retry': 'Try again',
378 // Label for process dialog retry action button, visible when describing only warnings
379 'ooui-dialog-process-continue': 'Continue',
380 // Label for button in combobox input that triggers its dropdown
381 'ooui-combobox-button-label': 'Dropdown for combobox',
382 // Label for the file selection widget's select file button
383 'ooui-selectfile-button-select': 'Select a file',
384 // Label for the file selection widget if file selection is not supported
385 'ooui-selectfile-not-supported': 'File selection is not supported',
386 // Label for the file selection widget when no file is currently selected
387 'ooui-selectfile-placeholder': 'No file is selected',
388 // Label for the file selection widget's drop target
389 'ooui-selectfile-dragdrop-placeholder': 'Drop file here',
390 // Label for the help icon attached to a form field
391 'ooui-field-help': 'Help'
395 * Get a localized message.
397 * After the message key, message parameters may optionally be passed. In the default implementation,
398 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
399 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
400 * they support unnamed, ordered message parameters.
402 * In environments that provide a localization system, this function should be overridden to
403 * return the message translated in the user's language. The default implementation always returns
404 * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
408 * var i, iLen, button,
409 * messagePath = 'oojs-ui/dist/i18n/',
410 * languages = [ $.i18n().locale, 'ur', 'en' ],
413 * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
414 * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
417 * $.i18n().load( languageMap ).done( function() {
418 * // Replace the built-in `msg` only once we've loaded the internationalization.
419 * // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
420 * // you put off creating any widgets until this promise is complete, no English
421 * // will be displayed.
422 * OO.ui.msg = $.i18n;
424 * // A button displaying "OK" in the default locale
425 * button = new OO.ui.ButtonWidget( {
426 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
429 * $( document.body ).append( button.$element );
431 * // A button displaying "OK" in Urdu
432 * $.i18n().locale = 'ur';
433 * button = new OO.ui.ButtonWidget( {
434 * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
437 * $( document.body ).append( button.$element );
440 * @param {string} key Message key
441 * @param {...Mixed} [params] Message parameters
442 * @return {string} Translated message with parameters substituted
444 OO
.ui
.msg = function ( key
) {
445 var message
= messages
[ key
],
446 params
= Array
.prototype.slice
.call( arguments
, 1 );
447 if ( typeof message
=== 'string' ) {
448 // Perform $1 substitution
449 message
= message
.replace( /\$(\d+)/g, function ( unused
, n
) {
450 var i
= parseInt( n
, 10 );
451 return params
[ i
- 1 ] !== undefined ? params
[ i
- 1 ] : '$' + n
;
454 // Return placeholder if message not found
455 message
= '[' + key
+ ']';
462 * Package a message and arguments for deferred resolution.
464 * Use this when you are statically specifying a message and the message may not yet be present.
466 * @param {string} key Message key
467 * @param {...Mixed} [params] Message parameters
468 * @return {Function} Function that returns the resolved message when executed
470 OO
.ui
.deferMsg = function () {
471 var args
= arguments
;
473 return OO
.ui
.msg
.apply( OO
.ui
, args
);
480 * If the message is a function it will be executed, otherwise it will pass through directly.
482 * @param {Function|string} msg Deferred message, or message text
483 * @return {string} Resolved message
485 OO
.ui
.resolveMsg = function ( msg
) {
486 if ( typeof msg
=== 'function' ) {
493 * @param {string} url
496 OO
.ui
.isSafeUrl = function ( url
) {
497 // Keep this function in sync with php/Tag.php
498 var i
, protocolWhitelist
;
500 function stringStartsWith( haystack
, needle
) {
501 return haystack
.substr( 0, needle
.length
) === needle
;
504 protocolWhitelist
= [
505 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
506 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
507 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
514 for ( i
= 0; i
< protocolWhitelist
.length
; i
++ ) {
515 if ( stringStartsWith( url
, protocolWhitelist
[ i
] + ':' ) ) {
520 // This matches '//' too
521 if ( stringStartsWith( url
, '/' ) || stringStartsWith( url
, './' ) ) {
524 if ( stringStartsWith( url
, '?' ) || stringStartsWith( url
, '#' ) ) {
532 * Check if the user has a 'mobile' device.
534 * For our purposes this means the user is primarily using an
535 * on-screen keyboard, touch input instead of a mouse and may
536 * have a physically small display.
538 * It is left up to implementors to decide how to compute this
539 * so the default implementation always returns false.
541 * @return {boolean} User is on a mobile device
543 OO
.ui
.isMobile = function () {
548 * Get the additional spacing that should be taken into account when displaying elements that are
549 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
550 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
552 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
553 * the extra spacing from that edge of viewport (in pixels)
555 OO
.ui
.getViewportSpacing = function () {
565 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
566 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
568 * @return {jQuery} Default overlay node
570 OO
.ui
.getDefaultOverlay = function () {
571 if ( !OO
.ui
.$defaultOverlay
) {
572 OO
.ui
.$defaultOverlay
= $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
573 $( document
.body
).append( OO
.ui
.$defaultOverlay
);
575 return OO
.ui
.$defaultOverlay
;
583 * Namespace for OOUI mixins.
585 * Mixins are named according to the type of object they are intended to
586 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
587 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
588 * is intended to be mixed in to an instance of OO.ui.Widget.
596 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
597 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
598 * connected to them and can't be interacted with.
604 * @param {Object} [config] Configuration options
605 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
606 * to the top level (e.g., the outermost div) of the element. See the [OOUI documentation on MediaWiki][2]
608 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#cssExample
609 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
610 * @cfg {string} [text] Text to insert
611 * @cfg {Array} [content] An array of content elements to append (after #text).
612 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
613 * Instances of OO.ui.Element will have their $element appended.
614 * @cfg {jQuery} [$content] Content elements to append (after #text).
615 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
616 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
617 * Data can also be specified with the #setData method.
619 OO
.ui
.Element
= function OoUiElement( config
) {
620 if ( OO
.ui
.isDemo
) {
621 this.initialConfig
= config
;
623 // Configuration initialization
624 config
= config
|| {};
628 this.elementId
= null;
630 this.data
= config
.data
;
631 this.$element
= config
.$element
||
632 $( document
.createElement( this.getTagName() ) );
633 this.elementGroup
= null;
636 if ( Array
.isArray( config
.classes
) ) {
637 this.$element
.addClass( config
.classes
);
640 this.setElementId( config
.id
);
643 this.$element
.text( config
.text
);
645 if ( config
.content
) {
646 // The `content` property treats plain strings as text; use an
647 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
648 // appropriate $element appended.
649 this.$element
.append( config
.content
.map( function ( v
) {
650 if ( typeof v
=== 'string' ) {
651 // Escape string so it is properly represented in HTML.
652 return document
.createTextNode( v
);
653 } else if ( v
instanceof OO
.ui
.HtmlSnippet
) {
656 } else if ( v
instanceof OO
.ui
.Element
) {
662 if ( config
.$content
) {
663 // The `$content` property treats plain strings as HTML.
664 this.$element
.append( config
.$content
);
670 OO
.initClass( OO
.ui
.Element
);
672 /* Static Properties */
675 * The name of the HTML tag used by the element.
677 * The static value may be ignored if the #getTagName method is overridden.
683 OO
.ui
.Element
.static.tagName
= 'div';
688 * Reconstitute a JavaScript object corresponding to a widget created
689 * by the PHP implementation.
691 * @param {string|HTMLElement|jQuery} idOrNode
692 * A DOM id (if a string) or node for the widget to infuse.
693 * @param {Object} [config] Configuration options
694 * @return {OO.ui.Element}
695 * The `OO.ui.Element` corresponding to this (infusable) document node.
696 * For `Tag` objects emitted on the HTML side (used occasionally for content)
697 * the value returned is a newly-created Element wrapping around the existing
700 OO
.ui
.Element
.static.infuse = function ( idOrNode
, config
) {
701 var obj
= OO
.ui
.Element
.static.unsafeInfuse( idOrNode
, config
, false );
703 if ( typeof idOrNode
=== 'string' ) {
704 // IDs deprecated since 0.29.7
705 OO
.ui
.warnDeprecation(
706 'Passing a string ID to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
709 // Verify that the type matches up.
710 // FIXME: uncomment after T89721 is fixed, see T90929.
712 if ( !( obj instanceof this['class'] ) ) {
713 throw new Error( 'Infusion type mismatch!' );
720 * Implementation helper for `infuse`; skips the type check and has an
721 * extra property so that only the top-level invocation touches the DOM.
724 * @param {string|HTMLElement|jQuery} idOrNode
725 * @param {Object} [config] Configuration options
726 * @param {jQuery.Promise} [domPromise] A promise that will be resolved
727 * when the top-level widget of this infusion is inserted into DOM,
728 * replacing the original node; only used internally.
729 * @return {OO.ui.Element}
731 OO
.ui
.Element
.static.unsafeInfuse = function ( idOrNode
, config
, domPromise
) {
732 // look for a cached result of a previous infusion.
733 var id
, $elem
, error
, data
, cls
, parts
, parent
, obj
, top
, state
, infusedChildren
;
734 if ( typeof idOrNode
=== 'string' ) {
736 $elem
= $( document
.getElementById( id
) );
738 $elem
= $( idOrNode
);
739 id
= $elem
.attr( 'id' );
741 if ( !$elem
.length
) {
742 if ( typeof idOrNode
=== 'string' ) {
743 error
= 'Widget not found: ' + idOrNode
;
744 } else if ( idOrNode
&& idOrNode
.selector
) {
745 error
= 'Widget not found: ' + idOrNode
.selector
;
747 error
= 'Widget not found';
749 throw new Error( error
);
751 if ( $elem
[ 0 ].oouiInfused
) {
752 $elem
= $elem
[ 0 ].oouiInfused
;
754 data
= $elem
.data( 'ooui-infused' );
757 if ( data
=== true ) {
758 throw new Error( 'Circular dependency! ' + id
);
761 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
762 state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
763 // restore dynamic state after the new element is re-inserted into DOM under infused parent
764 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
765 infusedChildren
= $elem
.data( 'ooui-infused-children' );
766 if ( infusedChildren
&& infusedChildren
.length
) {
767 infusedChildren
.forEach( function ( data
) {
768 var state
= data
.constructor.static.gatherPreInfuseState( $elem
, data
);
769 domPromise
.done( data
.restorePreInfuseState
.bind( data
, state
) );
775 data
= $elem
.attr( 'data-ooui' );
777 throw new Error( 'No infusion data found: ' + id
);
780 data
= JSON
.parse( data
);
784 if ( !( data
&& data
._
) ) {
785 throw new Error( 'No valid infusion data found: ' + id
);
787 if ( data
._
=== 'Tag' ) {
788 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
789 return new OO
.ui
.Element( $.extend( {}, config
, { $element
: $elem
} ) );
791 parts
= data
._
.split( '.' );
792 cls
= OO
.getProp
.apply( OO
, [ window
].concat( parts
) );
793 if ( cls
=== undefined ) {
794 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
797 // Verify that we're creating an OO.ui.Element instance
800 while ( parent
!== undefined ) {
801 if ( parent
=== OO
.ui
.Element
) {
806 parent
= parent
.parent
;
809 if ( parent
!== OO
.ui
.Element
) {
810 throw new Error( 'Unknown widget type: id: ' + id
+ ', class: ' + data
._
);
815 domPromise
= top
.promise();
817 $elem
.data( 'ooui-infused', true ); // prevent loops
818 data
.id
= id
; // implicit
819 infusedChildren
= [];
820 data
= OO
.copy( data
, null, function deserialize( value
) {
822 if ( OO
.isPlainObject( value
) ) {
824 infused
= OO
.ui
.Element
.static.unsafeInfuse( value
.tag
, config
, domPromise
);
825 infusedChildren
.push( infused
);
826 // Flatten the structure
827 infusedChildren
.push
.apply( infusedChildren
, infused
.$element
.data( 'ooui-infused-children' ) || [] );
828 infused
.$element
.removeData( 'ooui-infused-children' );
831 if ( value
.html
!== undefined ) {
832 return new OO
.ui
.HtmlSnippet( value
.html
);
836 // allow widgets to reuse parts of the DOM
837 data
= cls
.static.reusePreInfuseDOM( $elem
[ 0 ], data
);
838 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
839 state
= cls
.static.gatherPreInfuseState( $elem
[ 0 ], data
);
841 // eslint-disable-next-line new-cap
842 obj
= new cls( $.extend( {}, config
, data
) );
843 // If anyone is holding a reference to the old DOM element,
844 // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
845 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
846 $elem
[ 0 ].oouiInfused
= obj
.$element
;
847 // now replace old DOM with this new DOM.
849 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
850 // so only mutate the DOM if we need to.
851 if ( $elem
[ 0 ] !== obj
.$element
[ 0 ] ) {
852 $elem
.replaceWith( obj
.$element
);
856 obj
.$element
.data( 'ooui-infused', obj
);
857 obj
.$element
.data( 'ooui-infused-children', infusedChildren
);
858 // set the 'data-ooui' attribute so we can identify infused widgets
859 obj
.$element
.attr( 'data-ooui', '' );
860 // restore dynamic state after the new element is inserted into DOM
861 domPromise
.done( obj
.restorePreInfuseState
.bind( obj
, state
) );
866 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
868 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
869 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
870 * constructor, which will be given the enhanced config.
873 * @param {HTMLElement} node
874 * @param {Object} config
877 OO
.ui
.Element
.static.reusePreInfuseDOM = function ( node
, config
) {
882 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
883 * (and its children) that represent an Element of the same class and the given configuration,
884 * generated by the PHP implementation.
886 * This method is called just before `node` is detached from the DOM. The return value of this
887 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
888 * is inserted into DOM to replace `node`.
891 * @param {HTMLElement} node
892 * @param {Object} config
895 OO
.ui
.Element
.static.gatherPreInfuseState = function () {
900 * Get a jQuery function within a specific document.
903 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
904 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
906 * @return {Function} Bound jQuery function
908 OO
.ui
.Element
.static.getJQuery = function ( context
, $iframe
) {
909 function wrapper( selector
) {
910 return $( selector
, wrapper
.context
);
913 wrapper
.context
= this.getDocument( context
);
916 wrapper
.$iframe
= $iframe
;
923 * Get the document of an element.
926 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
927 * @return {HTMLDocument|null} Document object
929 OO
.ui
.Element
.static.getDocument = function ( obj
) {
930 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
931 return ( obj
[ 0 ] && obj
[ 0 ].ownerDocument
) ||
932 // Empty jQuery selections might have a context
939 ( obj
.nodeType
=== Node
.DOCUMENT_NODE
&& obj
) ||
944 * Get the window of an element or document.
947 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
948 * @return {Window} Window object
950 OO
.ui
.Element
.static.getWindow = function ( obj
) {
951 var doc
= this.getDocument( obj
);
952 return doc
.defaultView
;
956 * Get the direction of an element or document.
959 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
960 * @return {string} Text direction, either 'ltr' or 'rtl'
962 OO
.ui
.Element
.static.getDir = function ( obj
) {
965 if ( obj
instanceof $ ) {
968 isDoc
= obj
.nodeType
=== Node
.DOCUMENT_NODE
;
969 isWin
= obj
.document
!== undefined;
970 if ( isDoc
|| isWin
) {
976 return $( obj
).css( 'direction' );
980 * Get the offset between two frames.
982 * TODO: Make this function not use recursion.
985 * @param {Window} from Window of the child frame
986 * @param {Window} [to=window] Window of the parent frame
987 * @param {Object} [offset] Offset to start with, used internally
988 * @return {Object} Offset object, containing left and top properties
990 OO
.ui
.Element
.static.getFrameOffset = function ( from, to
, offset
) {
991 var i
, len
, frames
, frame
, rect
;
997 offset
= { top
: 0, left
: 0 };
999 if ( from.parent
=== from ) {
1003 // Get iframe element
1004 frames
= from.parent
.document
.getElementsByTagName( 'iframe' );
1005 for ( i
= 0, len
= frames
.length
; i
< len
; i
++ ) {
1006 if ( frames
[ i
].contentWindow
=== from ) {
1007 frame
= frames
[ i
];
1012 // Recursively accumulate offset values
1014 rect
= frame
.getBoundingClientRect();
1015 offset
.left
+= rect
.left
;
1016 offset
.top
+= rect
.top
;
1017 if ( from !== to
) {
1018 this.getFrameOffset( from.parent
, offset
);
1025 * Get the offset between two elements.
1027 * The two elements may be in a different frame, but in that case the frame $element is in must
1028 * be contained in the frame $anchor is in.
1031 * @param {jQuery} $element Element whose position to get
1032 * @param {jQuery} $anchor Element to get $element's position relative to
1033 * @return {Object} Translated position coordinates, containing top and left properties
1035 OO
.ui
.Element
.static.getRelativePosition = function ( $element
, $anchor
) {
1036 var iframe
, iframePos
,
1037 pos
= $element
.offset(),
1038 anchorPos
= $anchor
.offset(),
1039 elementDocument
= this.getDocument( $element
),
1040 anchorDocument
= this.getDocument( $anchor
);
1042 // If $element isn't in the same document as $anchor, traverse up
1043 while ( elementDocument
!== anchorDocument
) {
1044 iframe
= elementDocument
.defaultView
.frameElement
;
1046 throw new Error( '$element frame is not contained in $anchor frame' );
1048 iframePos
= $( iframe
).offset();
1049 pos
.left
+= iframePos
.left
;
1050 pos
.top
+= iframePos
.top
;
1051 elementDocument
= iframe
.ownerDocument
;
1053 pos
.left
-= anchorPos
.left
;
1054 pos
.top
-= anchorPos
.top
;
1059 * Get element border sizes.
1062 * @param {HTMLElement} el Element to measure
1063 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1065 OO
.ui
.Element
.static.getBorders = function ( el
) {
1066 var doc
= el
.ownerDocument
,
1067 win
= doc
.defaultView
,
1068 style
= win
.getComputedStyle( el
, null ),
1070 top
= parseFloat( style
? style
.borderTopWidth
: $el
.css( 'borderTopWidth' ) ) || 0,
1071 left
= parseFloat( style
? style
.borderLeftWidth
: $el
.css( 'borderLeftWidth' ) ) || 0,
1072 bottom
= parseFloat( style
? style
.borderBottomWidth
: $el
.css( 'borderBottomWidth' ) ) || 0,
1073 right
= parseFloat( style
? style
.borderRightWidth
: $el
.css( 'borderRightWidth' ) ) || 0;
1084 * Get dimensions of an element or window.
1087 * @param {HTMLElement|Window} el Element to measure
1088 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1090 OO
.ui
.Element
.static.getDimensions = function ( el
) {
1092 doc
= el
.ownerDocument
|| el
.document
,
1093 win
= doc
.defaultView
;
1095 if ( win
=== el
|| el
=== doc
.documentElement
) {
1098 borders
: { top
: 0, left
: 0, bottom
: 0, right
: 0 },
1100 top
: $win
.scrollTop(),
1101 left
: $win
.scrollLeft()
1103 scrollbar
: { right
: 0, bottom
: 0 },
1107 bottom
: $win
.innerHeight(),
1108 right
: $win
.innerWidth()
1114 borders
: this.getBorders( el
),
1116 top
: $el
.scrollTop(),
1117 left
: $el
.scrollLeft()
1120 right
: $el
.innerWidth() - el
.clientWidth
,
1121 bottom
: $el
.innerHeight() - el
.clientHeight
1123 rect
: el
.getBoundingClientRect()
1129 * Get the number of pixels that an element's content is scrolled to the left.
1131 * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
1132 * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
1134 * This function smooths out browser inconsistencies (nicely described in the README at
1135 * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
1136 * with Firefox's 'scrollLeft', which seems the sanest.
1140 * @param {HTMLElement|Window} el Element to measure
1141 * @return {number} Scroll position from the left.
1142 * If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
1143 * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
1144 * If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
1145 * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
1147 OO
.ui
.Element
.static.getScrollLeft
= ( function () {
1148 var rtlScrollType
= null;
1151 var $definer
= $( '<div>' ).attr( {
1153 style
: 'font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
1155 definer
= $definer
[ 0 ];
1157 $definer
.appendTo( 'body' );
1158 if ( definer
.scrollLeft
> 0 ) {
1160 rtlScrollType
= 'default';
1162 definer
.scrollLeft
= 1;
1163 if ( definer
.scrollLeft
=== 0 ) {
1164 // Firefox, old Opera
1165 rtlScrollType
= 'negative';
1167 // Internet Explorer, Edge
1168 rtlScrollType
= 'reverse';
1174 return function getScrollLeft( el
) {
1175 var isRoot
= el
.window
=== el
||
1176 el
=== el
.ownerDocument
.body
||
1177 el
=== el
.ownerDocument
.documentElement
,
1178 scrollLeft
= isRoot
? $( window
).scrollLeft() : el
.scrollLeft
,
1179 // All browsers use the correct scroll type ('negative') on the root, so don't
1180 // do any fixups when looking at the root element
1181 direction
= isRoot
? 'ltr' : $( el
).css( 'direction' );
1183 if ( direction
=== 'rtl' ) {
1184 if ( rtlScrollType
=== null ) {
1187 if ( rtlScrollType
=== 'reverse' ) {
1188 scrollLeft
= -scrollLeft
;
1189 } else if ( rtlScrollType
=== 'default' ) {
1190 scrollLeft
= scrollLeft
- el
.scrollWidth
+ el
.clientWidth
;
1199 * Get the root scrollable element of given element's document.
1201 * On Blink-based browsers (Chrome etc.), `document.documentElement` can't be used to get or set
1202 * the scrollTop property; instead we have to use `document.body`. Changing and testing the value
1203 * lets us use 'body' or 'documentElement' based on what is working.
1205 * https://code.google.com/p/chromium/issues/detail?id=303131
1208 * @param {HTMLElement} el Element to find root scrollable parent for
1209 * @return {HTMLElement} Scrollable parent, `document.body` or `document.documentElement`
1210 * depending on browser
1212 OO
.ui
.Element
.static.getRootScrollableElement = function ( el
) {
1213 var scrollTop
, body
;
1215 if ( OO
.ui
.scrollableElement
=== undefined ) {
1216 body
= el
.ownerDocument
.body
;
1217 scrollTop
= body
.scrollTop
;
1220 // In some browsers (observed in Chrome 56 on Linux Mint 18.1),
1221 // body.scrollTop doesn't become exactly 1, but a fractional value like 0.76
1222 if ( Math
.round( body
.scrollTop
) === 1 ) {
1223 body
.scrollTop
= scrollTop
;
1224 OO
.ui
.scrollableElement
= 'body';
1226 OO
.ui
.scrollableElement
= 'documentElement';
1230 return el
.ownerDocument
[ OO
.ui
.scrollableElement
];
1234 * Get closest scrollable container.
1236 * Traverses up until either a scrollable element or the root is reached, in which case the root
1237 * scrollable element will be returned (see #getRootScrollableElement).
1240 * @param {HTMLElement} el Element to find scrollable container for
1241 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1242 * @return {HTMLElement} Closest scrollable container
1244 OO
.ui
.Element
.static.getClosestScrollableContainer = function ( el
, dimension
) {
1246 // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
1247 // 'overflow-y' have different values, so we need to check the separate properties.
1248 props
= [ 'overflow-x', 'overflow-y' ],
1249 $parent
= $( el
).parent();
1251 if ( dimension
=== 'x' || dimension
=== 'y' ) {
1252 props
= [ 'overflow-' + dimension
];
1255 // Special case for the document root (which doesn't really have any scrollable container, since
1256 // it is the ultimate scrollable container, but this is probably saner than null or exception)
1257 if ( $( el
).is( 'html, body' ) ) {
1258 return this.getRootScrollableElement( el
);
1261 while ( $parent
.length
) {
1262 if ( $parent
[ 0 ] === this.getRootScrollableElement( el
) ) {
1263 return $parent
[ 0 ];
1267 val
= $parent
.css( props
[ i
] );
1268 // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
1269 // scrolled in that direction, but they can actually be scrolled programatically. The user can
1270 // unintentionally perform a scroll in such case even if the application doesn't scroll
1271 // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
1272 // This could cause funny issues...
1273 if ( val
=== 'auto' || val
=== 'scroll' ) {
1274 return $parent
[ 0 ];
1277 $parent
= $parent
.parent();
1279 // The element is unattached... return something mostly sane
1280 return this.getRootScrollableElement( el
);
1284 * Scroll element into view.
1287 * @param {HTMLElement} el Element to scroll into view
1288 * @param {Object} [config] Configuration options
1289 * @param {string} [config.duration='fast'] jQuery animation duration value
1290 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1291 * to scroll in both directions
1292 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1294 OO
.ui
.Element
.static.scrollIntoView = function ( el
, config
) {
1295 var position
, animations
, container
, $container
, elementDimensions
, containerDimensions
, $window
,
1296 deferred
= $.Deferred();
1298 // Configuration initialization
1299 config
= config
|| {};
1302 container
= this.getClosestScrollableContainer( el
, config
.direction
);
1303 $container
= $( container
);
1304 elementDimensions
= this.getDimensions( el
);
1305 containerDimensions
= this.getDimensions( container
);
1306 $window
= $( this.getWindow( el
) );
1308 // Compute the element's position relative to the container
1309 if ( $container
.is( 'html, body' ) ) {
1310 // If the scrollable container is the root, this is easy
1312 top
: elementDimensions
.rect
.top
,
1313 bottom
: $window
.innerHeight() - elementDimensions
.rect
.bottom
,
1314 left
: elementDimensions
.rect
.left
,
1315 right
: $window
.innerWidth() - elementDimensions
.rect
.right
1318 // Otherwise, we have to subtract el's coordinates from container's coordinates
1320 top
: elementDimensions
.rect
.top
- ( containerDimensions
.rect
.top
+ containerDimensions
.borders
.top
),
1321 bottom
: containerDimensions
.rect
.bottom
- containerDimensions
.borders
.bottom
- containerDimensions
.scrollbar
.bottom
- elementDimensions
.rect
.bottom
,
1322 left
: elementDimensions
.rect
.left
- ( containerDimensions
.rect
.left
+ containerDimensions
.borders
.left
),
1323 right
: containerDimensions
.rect
.right
- containerDimensions
.borders
.right
- containerDimensions
.scrollbar
.right
- elementDimensions
.rect
.right
1327 if ( !config
.direction
|| config
.direction
=== 'y' ) {
1328 if ( position
.top
< 0 ) {
1329 animations
.scrollTop
= containerDimensions
.scroll
.top
+ position
.top
;
1330 } else if ( position
.top
> 0 && position
.bottom
< 0 ) {
1331 animations
.scrollTop
= containerDimensions
.scroll
.top
+ Math
.min( position
.top
, -position
.bottom
);
1334 if ( !config
.direction
|| config
.direction
=== 'x' ) {
1335 if ( position
.left
< 0 ) {
1336 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ position
.left
;
1337 } else if ( position
.left
> 0 && position
.right
< 0 ) {
1338 animations
.scrollLeft
= containerDimensions
.scroll
.left
+ Math
.min( position
.left
, -position
.right
);
1341 if ( !$.isEmptyObject( animations
) ) {
1342 // eslint-disable-next-line jquery/no-animate
1343 $container
.stop( true ).animate( animations
, config
.duration
=== undefined ? 'fast' : config
.duration
);
1344 $container
.queue( function ( next
) {
1351 return deferred
.promise();
1355 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1356 * and reserve space for them, because it probably doesn't.
1358 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1359 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1360 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1361 * and then reattach (or show) them back.
1364 * @param {HTMLElement} el Element to reconsider the scrollbars on
1366 OO
.ui
.Element
.static.reconsiderScrollbars = function ( el
) {
1367 var i
, len
, scrollLeft
, scrollTop
, nodes
= [];
1368 // Save scroll position
1369 scrollLeft
= el
.scrollLeft
;
1370 scrollTop
= el
.scrollTop
;
1371 // Detach all children
1372 while ( el
.firstChild
) {
1373 nodes
.push( el
.firstChild
);
1374 el
.removeChild( el
.firstChild
);
1377 // eslint-disable-next-line no-void
1378 void el
.offsetHeight
;
1379 // Reattach all children
1380 for ( i
= 0, len
= nodes
.length
; i
< len
; i
++ ) {
1381 el
.appendChild( nodes
[ i
] );
1383 // Restore scroll position (no-op if scrollbars disappeared)
1384 el
.scrollLeft
= scrollLeft
;
1385 el
.scrollTop
= scrollTop
;
1391 * Toggle visibility of an element.
1393 * @param {boolean} [show] Make element visible, omit to toggle visibility
1396 * @return {OO.ui.Element} The element, for chaining
1398 OO
.ui
.Element
.prototype.toggle = function ( show
) {
1399 show
= show
=== undefined ? !this.visible
: !!show
;
1401 if ( show
!== this.isVisible() ) {
1402 this.visible
= show
;
1403 this.$element
.toggleClass( 'oo-ui-element-hidden', !this.visible
);
1404 this.emit( 'toggle', show
);
1411 * Check if element is visible.
1413 * @return {boolean} element is visible
1415 OO
.ui
.Element
.prototype.isVisible = function () {
1416 return this.visible
;
1422 * @return {Mixed} Element data
1424 OO
.ui
.Element
.prototype.getData = function () {
1431 * @param {Mixed} data Element data
1433 * @return {OO.ui.Element} The element, for chaining
1435 OO
.ui
.Element
.prototype.setData = function ( data
) {
1441 * Set the element has an 'id' attribute.
1443 * @param {string} id
1445 * @return {OO.ui.Element} The element, for chaining
1447 OO
.ui
.Element
.prototype.setElementId = function ( id
) {
1448 this.elementId
= id
;
1449 this.$element
.attr( 'id', id
);
1454 * Ensure that the element has an 'id' attribute, setting it to an unique value if it's missing,
1455 * and return its value.
1459 OO
.ui
.Element
.prototype.getElementId = function () {
1460 if ( this.elementId
=== null ) {
1461 this.setElementId( OO
.ui
.generateElementId() );
1463 return this.elementId
;
1467 * Check if element supports one or more methods.
1469 * @param {string|string[]} methods Method or list of methods to check
1470 * @return {boolean} All methods are supported
1472 OO
.ui
.Element
.prototype.supports = function ( methods
) {
1476 methods
= Array
.isArray( methods
) ? methods
: [ methods
];
1477 for ( i
= 0, len
= methods
.length
; i
< len
; i
++ ) {
1478 if ( typeof this[ methods
[ i
] ] === 'function' ) {
1483 return methods
.length
=== support
;
1487 * Update the theme-provided classes.
1489 * @localdoc This is called in element mixins and widget classes any time state changes.
1490 * Updating is debounced, minimizing overhead of changing multiple attributes and
1491 * guaranteeing that theme updates do not occur within an element's constructor
1493 OO
.ui
.Element
.prototype.updateThemeClasses = function () {
1494 OO
.ui
.theme
.queueUpdateElementClasses( this );
1498 * Get the HTML tag name.
1500 * Override this method to base the result on instance information.
1502 * @return {string} HTML tag name
1504 OO
.ui
.Element
.prototype.getTagName = function () {
1505 return this.constructor.static.tagName
;
1509 * Check if the element is attached to the DOM
1511 * @return {boolean} The element is attached to the DOM
1513 OO
.ui
.Element
.prototype.isElementAttached = function () {
1514 return $.contains( this.getElementDocument(), this.$element
[ 0 ] );
1518 * Get the DOM document.
1520 * @return {HTMLDocument} Document object
1522 OO
.ui
.Element
.prototype.getElementDocument = function () {
1523 // Don't cache this in other ways either because subclasses could can change this.$element
1524 return OO
.ui
.Element
.static.getDocument( this.$element
);
1528 * Get the DOM window.
1530 * @return {Window} Window object
1532 OO
.ui
.Element
.prototype.getElementWindow = function () {
1533 return OO
.ui
.Element
.static.getWindow( this.$element
);
1537 * Get closest scrollable container.
1539 * @return {HTMLElement} Closest scrollable container
1541 OO
.ui
.Element
.prototype.getClosestScrollableElementContainer = function () {
1542 return OO
.ui
.Element
.static.getClosestScrollableContainer( this.$element
[ 0 ] );
1546 * Get group element is in.
1548 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1550 OO
.ui
.Element
.prototype.getElementGroup = function () {
1551 return this.elementGroup
;
1555 * Set group element is in.
1557 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1559 * @return {OO.ui.Element} The element, for chaining
1561 OO
.ui
.Element
.prototype.setElementGroup = function ( group
) {
1562 this.elementGroup
= group
;
1567 * Scroll element into view.
1569 * @param {Object} [config] Configuration options
1570 * @return {jQuery.Promise} Promise which resolves when the scroll is complete
1572 OO
.ui
.Element
.prototype.scrollElementIntoView = function ( config
) {
1574 !this.isElementAttached() ||
1575 !this.isVisible() ||
1576 ( this.getElementGroup() && !this.getElementGroup().isVisible() )
1578 return $.Deferred().resolve();
1580 return OO
.ui
.Element
.static.scrollIntoView( this.$element
[ 0 ], config
);
1584 * Restore the pre-infusion dynamic state for this widget.
1586 * This method is called after #$element has been inserted into DOM. The parameter is the return
1587 * value of #gatherPreInfuseState.
1590 * @param {Object} state
1592 OO
.ui
.Element
.prototype.restorePreInfuseState = function () {
1596 * Wraps an HTML snippet for use with configuration values which default
1597 * to strings. This bypasses the default html-escaping done to string
1603 * @param {string} [content] HTML content
1605 OO
.ui
.HtmlSnippet
= function OoUiHtmlSnippet( content
) {
1607 this.content
= content
;
1612 OO
.initClass( OO
.ui
.HtmlSnippet
);
1619 * @return {string} Unchanged HTML snippet.
1621 OO
.ui
.HtmlSnippet
.prototype.toString = function () {
1622 return this.content
;
1626 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1627 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1628 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1629 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1630 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1634 * @extends OO.ui.Element
1635 * @mixins OO.EventEmitter
1638 * @param {Object} [config] Configuration options
1640 OO
.ui
.Layout
= function OoUiLayout( config
) {
1641 // Configuration initialization
1642 config
= config
|| {};
1644 // Parent constructor
1645 OO
.ui
.Layout
.parent
.call( this, config
);
1647 // Mixin constructors
1648 OO
.EventEmitter
.call( this );
1651 this.$element
.addClass( 'oo-ui-layout' );
1656 OO
.inheritClass( OO
.ui
.Layout
, OO
.ui
.Element
);
1657 OO
.mixinClass( OO
.ui
.Layout
, OO
.EventEmitter
);
1662 * Reset scroll offsets
1665 * @return {OO.ui.Layout} The layout, for chaining
1667 OO
.ui
.Layout
.prototype.resetScroll = function () {
1668 this.$element
[ 0 ].scrollTop
= 0;
1669 // TODO: Reset scrollLeft in an RTL-aware manner, see OO.ui.Element.static.getScrollLeft.
1675 * Widgets are compositions of one or more OOUI elements that users can both view
1676 * and interact with. All widgets can be configured and modified via a standard API,
1677 * and their state can change dynamically according to a model.
1681 * @extends OO.ui.Element
1682 * @mixins OO.EventEmitter
1685 * @param {Object} [config] Configuration options
1686 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1687 * appearance reflects this state.
1689 OO
.ui
.Widget
= function OoUiWidget( config
) {
1690 // Initialize config
1691 config
= $.extend( { disabled
: false }, config
);
1693 // Parent constructor
1694 OO
.ui
.Widget
.parent
.call( this, config
);
1696 // Mixin constructors
1697 OO
.EventEmitter
.call( this );
1700 this.disabled
= null;
1701 this.wasDisabled
= null;
1704 this.$element
.addClass( 'oo-ui-widget' );
1705 this.setDisabled( !!config
.disabled
);
1710 OO
.inheritClass( OO
.ui
.Widget
, OO
.ui
.Element
);
1711 OO
.mixinClass( OO
.ui
.Widget
, OO
.EventEmitter
);
1718 * A 'disable' event is emitted when the disabled state of the widget changes
1719 * (i.e. on disable **and** enable).
1721 * @param {boolean} disabled Widget is disabled
1727 * A 'toggle' event is emitted when the visibility of the widget changes.
1729 * @param {boolean} visible Widget is visible
1735 * Check if the widget is disabled.
1737 * @return {boolean} Widget is disabled
1739 OO
.ui
.Widget
.prototype.isDisabled = function () {
1740 return this.disabled
;
1744 * Set the 'disabled' state of the widget.
1746 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1748 * @param {boolean} disabled Disable widget
1750 * @return {OO.ui.Widget} The widget, for chaining
1752 OO
.ui
.Widget
.prototype.setDisabled = function ( disabled
) {
1755 this.disabled
= !!disabled
;
1756 isDisabled
= this.isDisabled();
1757 if ( isDisabled
!== this.wasDisabled
) {
1758 this.$element
.toggleClass( 'oo-ui-widget-disabled', isDisabled
);
1759 this.$element
.toggleClass( 'oo-ui-widget-enabled', !isDisabled
);
1760 this.$element
.attr( 'aria-disabled', isDisabled
.toString() );
1761 this.emit( 'disable', isDisabled
);
1762 this.updateThemeClasses();
1764 this.wasDisabled
= isDisabled
;
1770 * Update the disabled state, in case of changes in parent widget.
1773 * @return {OO.ui.Widget} The widget, for chaining
1775 OO
.ui
.Widget
.prototype.updateDisabled = function () {
1776 this.setDisabled( this.disabled
);
1781 * Get an ID of a labelable node which is part of this widget, if any, to be used for `<label for>`
1784 * If this function returns null, the widget should have a meaningful #simulateLabelClick method
1787 * @return {string|null} The ID of the labelable element
1789 OO
.ui
.Widget
.prototype.getInputId = function () {
1794 * Simulate the behavior of clicking on a label (a HTML `<label>` element) bound to this input.
1795 * HTML only allows `<label>` to act on specific "labelable" elements; complex widgets might need to
1796 * override this method to provide intuitive, accessible behavior.
1798 * By default, this does nothing. OO.ui.mixin.TabIndexedElement overrides it for focusable widgets.
1799 * Individual widgets may override it too.
1801 * This method is called by OO.ui.LabelWidget and OO.ui.FieldLayout. It should not be called
1804 OO
.ui
.Widget
.prototype.simulateLabelClick = function () {
1815 OO
.ui
.Theme
= function OoUiTheme() {
1816 this.elementClassesQueue
= [];
1817 this.debouncedUpdateQueuedElementClasses
= OO
.ui
.debounce( this.updateQueuedElementClasses
);
1822 OO
.initClass( OO
.ui
.Theme
);
1827 * Get a list of classes to be applied to a widget.
1829 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1830 * otherwise state transitions will not work properly.
1832 * @param {OO.ui.Element} element Element for which to get classes
1833 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1835 OO
.ui
.Theme
.prototype.getElementClasses = function () {
1836 return { on
: [], off
: [] };
1840 * Update CSS classes provided by the theme.
1842 * For elements with theme logic hooks, this should be called any time there's a state change.
1844 * @param {OO.ui.Element} element Element for which to update classes
1846 OO
.ui
.Theme
.prototype.updateElementClasses = function ( element
) {
1847 var $elements
= $( [] ),
1848 classes
= this.getElementClasses( element
);
1850 if ( element
.$icon
) {
1851 $elements
= $elements
.add( element
.$icon
);
1853 if ( element
.$indicator
) {
1854 $elements
= $elements
.add( element
.$indicator
);
1858 .removeClass( classes
.off
)
1859 .addClass( classes
.on
);
1865 OO
.ui
.Theme
.prototype.updateQueuedElementClasses = function () {
1867 for ( i
= 0; i
< this.elementClassesQueue
.length
; i
++ ) {
1868 this.updateElementClasses( this.elementClassesQueue
[ i
] );
1871 this.elementClassesQueue
= [];
1875 * Queue #updateElementClasses to be called for this element.
1877 * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
1878 * to make them synchronous.
1880 * @param {OO.ui.Element} element Element for which to update classes
1882 OO
.ui
.Theme
.prototype.queueUpdateElementClasses = function ( element
) {
1883 // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
1884 // the most common case (this method is often called repeatedly for the same element).
1885 if ( this.elementClassesQueue
.lastIndexOf( element
) !== -1 ) {
1888 this.elementClassesQueue
.push( element
);
1889 this.debouncedUpdateQueuedElementClasses();
1893 * Get the transition duration in milliseconds for dialogs opening/closing
1895 * The dialog should be fully rendered this many milliseconds after the
1896 * ready process has executed.
1898 * @return {number} Transition duration in milliseconds
1900 OO
.ui
.Theme
.prototype.getDialogTransitionDuration = function () {
1905 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1906 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1907 * order in which users will navigate through the focusable elements via the “tab” key.
1910 * // TabIndexedElement is mixed into the ButtonWidget class
1911 * // to provide a tabIndex property.
1912 * var button1 = new OO.ui.ButtonWidget( {
1916 * button2 = new OO.ui.ButtonWidget( {
1920 * button3 = new OO.ui.ButtonWidget( {
1924 * button4 = new OO.ui.ButtonWidget( {
1928 * $( document.body ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1934 * @param {Object} [config] Configuration options
1935 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1936 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1937 * functionality will be applied to it instead.
1938 * @cfg {string|number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1939 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1940 * to remove the element from the tab-navigation flow.
1942 OO
.ui
.mixin
.TabIndexedElement
= function OoUiMixinTabIndexedElement( config
) {
1943 // Configuration initialization
1944 config
= $.extend( { tabIndex
: 0 }, config
);
1947 this.$tabIndexed
= null;
1948 this.tabIndex
= null;
1951 this.connect( this, { disable
: 'onTabIndexedElementDisable' } );
1954 this.setTabIndex( config
.tabIndex
);
1955 this.setTabIndexedElement( config
.$tabIndexed
|| this.$element
);
1960 OO
.initClass( OO
.ui
.mixin
.TabIndexedElement
);
1965 * Set the element that should use the tabindex functionality.
1967 * This method is used to retarget a tabindex mixin so that its functionality applies
1968 * to the specified element. If an element is currently using the functionality, the mixin’s
1969 * effect on that element is removed before the new element is set up.
1971 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1973 * @return {OO.ui.Element} The element, for chaining
1975 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndexedElement = function ( $tabIndexed
) {
1976 var tabIndex
= this.tabIndex
;
1977 // Remove attributes from old $tabIndexed
1978 this.setTabIndex( null );
1979 // Force update of new $tabIndexed
1980 this.$tabIndexed
= $tabIndexed
;
1981 this.tabIndex
= tabIndex
;
1982 return this.updateTabIndex();
1986 * Set the value of the tabindex.
1988 * @param {string|number|null} tabIndex Tabindex value, or `null` for no tabindex
1990 * @return {OO.ui.Element} The element, for chaining
1992 OO
.ui
.mixin
.TabIndexedElement
.prototype.setTabIndex = function ( tabIndex
) {
1993 tabIndex
= /^-?\d+$/.test( tabIndex
) ? Number( tabIndex
) : null;
1995 if ( this.tabIndex
!== tabIndex
) {
1996 this.tabIndex
= tabIndex
;
1997 this.updateTabIndex();
2004 * Update the `tabindex` attribute, in case of changes to tab index or
2009 * @return {OO.ui.Element} The element, for chaining
2011 OO
.ui
.mixin
.TabIndexedElement
.prototype.updateTabIndex = function () {
2012 if ( this.$tabIndexed
) {
2013 if ( this.tabIndex
!== null ) {
2014 // Do not index over disabled elements
2015 this.$tabIndexed
.attr( {
2016 tabindex
: this.isDisabled() ? -1 : this.tabIndex
,
2017 // Support: ChromeVox and NVDA
2018 // These do not seem to inherit aria-disabled from parent elements
2019 'aria-disabled': this.isDisabled().toString()
2022 this.$tabIndexed
.removeAttr( 'tabindex aria-disabled' );
2029 * Handle disable events.
2032 * @param {boolean} disabled Element is disabled
2034 OO
.ui
.mixin
.TabIndexedElement
.prototype.onTabIndexedElementDisable = function () {
2035 this.updateTabIndex();
2039 * Get the value of the tabindex.
2041 * @return {number|null} Tabindex value
2043 OO
.ui
.mixin
.TabIndexedElement
.prototype.getTabIndex = function () {
2044 return this.tabIndex
;
2048 * Get an ID of a focusable element of this widget, if any, to be used for `<label for>` value.
2050 * If the element already has an ID then that is returned, otherwise unique ID is
2051 * generated, set on the element, and returned.
2053 * @return {string|null} The ID of the focusable element
2055 OO
.ui
.mixin
.TabIndexedElement
.prototype.getInputId = function () {
2058 if ( !this.$tabIndexed
) {
2061 if ( !this.isLabelableNode( this.$tabIndexed
) ) {
2065 id
= this.$tabIndexed
.attr( 'id' );
2066 if ( id
=== undefined ) {
2067 id
= OO
.ui
.generateElementId();
2068 this.$tabIndexed
.attr( 'id', id
);
2075 * Whether the node is 'labelable' according to the HTML spec
2076 * (i.e., whether it can be interacted with through a `<label for="…">`).
2077 * See: <https://html.spec.whatwg.org/multipage/forms.html#category-label>.
2080 * @param {jQuery} $node
2083 OO
.ui
.mixin
.TabIndexedElement
.prototype.isLabelableNode = function ( $node
) {
2085 labelableTags
= [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
2086 tagName
= $node
.prop( 'tagName' ).toLowerCase();
2088 if ( tagName
=== 'input' && $node
.attr( 'type' ) !== 'hidden' ) {
2091 if ( labelableTags
.indexOf( tagName
) !== -1 ) {
2098 * Focus this element.
2101 * @return {OO.ui.Element} The element, for chaining
2103 OO
.ui
.mixin
.TabIndexedElement
.prototype.focus = function () {
2104 if ( !this.isDisabled() ) {
2105 this.$tabIndexed
.focus();
2111 * Blur this element.
2114 * @return {OO.ui.Element} The element, for chaining
2116 OO
.ui
.mixin
.TabIndexedElement
.prototype.blur = function () {
2117 this.$tabIndexed
.blur();
2122 * @inheritdoc OO.ui.Widget
2124 OO
.ui
.mixin
.TabIndexedElement
.prototype.simulateLabelClick = function () {
2129 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
2130 * interface element that can be configured with access keys for keyboard interaction.
2131 * See the [OOUI documentation on MediaWiki] [1] for examples.
2133 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
2139 * @param {Object} [config] Configuration options
2140 * @cfg {jQuery} [$button] The button element created by the class.
2141 * If this configuration is omitted, the button element will use a generated `<a>`.
2142 * @cfg {boolean} [framed=true] Render the button with a frame
2144 OO
.ui
.mixin
.ButtonElement
= function OoUiMixinButtonElement( config
) {
2145 // Configuration initialization
2146 config
= config
|| {};
2149 this.$button
= null;
2151 this.active
= config
.active
!== undefined && config
.active
;
2152 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
2153 this.onMouseDownHandler
= this.onMouseDown
.bind( this );
2154 this.onDocumentKeyUpHandler
= this.onDocumentKeyUp
.bind( this );
2155 this.onKeyDownHandler
= this.onKeyDown
.bind( this );
2156 this.onClickHandler
= this.onClick
.bind( this );
2157 this.onKeyPressHandler
= this.onKeyPress
.bind( this );
2160 this.$element
.addClass( 'oo-ui-buttonElement' );
2161 this.toggleFramed( config
.framed
=== undefined || config
.framed
);
2162 this.setButtonElement( config
.$button
|| $( '<a>' ) );
2167 OO
.initClass( OO
.ui
.mixin
.ButtonElement
);
2169 /* Static Properties */
2172 * Cancel mouse down events.
2174 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
2175 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
2176 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
2181 * @property {boolean}
2183 OO
.ui
.mixin
.ButtonElement
.static.cancelButtonMouseDownEvents
= true;
2188 * A 'click' event is emitted when the button element is clicked.
2196 * Set the button element.
2198 * This method is used to retarget a button mixin so that its functionality applies to
2199 * the specified button element instead of the one created by the class. If a button element
2200 * is already set, the method will remove the mixin’s effect on that element.
2202 * @param {jQuery} $button Element to use as button
2204 OO
.ui
.mixin
.ButtonElement
.prototype.setButtonElement = function ( $button
) {
2205 if ( this.$button
) {
2207 .removeClass( 'oo-ui-buttonElement-button' )
2208 .removeAttr( 'role accesskey' )
2210 mousedown
: this.onMouseDownHandler
,
2211 keydown
: this.onKeyDownHandler
,
2212 click
: this.onClickHandler
,
2213 keypress
: this.onKeyPressHandler
2217 this.$button
= $button
2218 .addClass( 'oo-ui-buttonElement-button' )
2220 mousedown
: this.onMouseDownHandler
,
2221 keydown
: this.onKeyDownHandler
,
2222 click
: this.onClickHandler
,
2223 keypress
: this.onKeyPressHandler
2226 // Add `role="button"` on `<a>` elements, where it's needed
2227 // `toUpperCase()` is added for XHTML documents
2228 if ( this.$button
.prop( 'tagName' ).toUpperCase() === 'A' ) {
2229 this.$button
.attr( 'role', 'button' );
2234 * Handles mouse down events.
2237 * @param {jQuery.Event} e Mouse down event
2238 * @return {undefined/boolean} False to prevent default if event is handled
2240 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseDown = function ( e
) {
2241 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2244 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2245 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
2246 // reliably remove the pressed class
2247 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2248 // Prevent change of focus unless specifically configured otherwise
2249 if ( this.constructor.static.cancelButtonMouseDownEvents
) {
2255 * Handles document mouse up events.
2258 * @param {MouseEvent} e Mouse up event
2260 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentMouseUp = function ( e
) {
2261 if ( this.isDisabled() || e
.which
!== OO
.ui
.MouseButtons
.LEFT
) {
2264 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2265 // Stop listening for mouseup, since we only needed this once
2266 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
2269 // Deprecated alias since 0.28.3
2270 OO
.ui
.mixin
.ButtonElement
.prototype.onMouseUp = function () {
2271 OO
.ui
.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
2272 this.onDocumentMouseUp
.apply( this, arguments
);
2276 * Handles mouse click events.
2279 * @param {jQuery.Event} e Mouse click event
2281 * @return {undefined/boolean} False to prevent default if event is handled
2283 OO
.ui
.mixin
.ButtonElement
.prototype.onClick = function ( e
) {
2284 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
2285 if ( this.emit( 'click' ) ) {
2292 * Handles key down events.
2295 * @param {jQuery.Event} e Key down event
2297 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyDown = function ( e
) {
2298 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2301 this.$element
.addClass( 'oo-ui-buttonElement-pressed' );
2302 // Run the keyup handler no matter where the key is when the button is let go, so we can
2303 // reliably remove the pressed class
2304 this.getElementDocument().addEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2308 * Handles document key up events.
2311 * @param {KeyboardEvent} e Key up event
2313 OO
.ui
.mixin
.ButtonElement
.prototype.onDocumentKeyUp = function ( e
) {
2314 if ( this.isDisabled() || ( e
.which
!== OO
.ui
.Keys
.SPACE
&& e
.which
!== OO
.ui
.Keys
.ENTER
) ) {
2317 this.$element
.removeClass( 'oo-ui-buttonElement-pressed' );
2318 // Stop listening for keyup, since we only needed this once
2319 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler
, true );
2322 // Deprecated alias since 0.28.3
2323 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyUp = function () {
2324 OO
.ui
.warnDeprecation( 'onKeyUp is deprecated, use onDocumentKeyUp instead' );
2325 this.onDocumentKeyUp
.apply( this, arguments
);
2329 * Handles key press events.
2332 * @param {jQuery.Event} e Key press event
2334 * @return {undefined/boolean} False to prevent default if event is handled
2336 OO
.ui
.mixin
.ButtonElement
.prototype.onKeyPress = function ( e
) {
2337 if ( !this.isDisabled() && ( e
.which
=== OO
.ui
.Keys
.SPACE
|| e
.which
=== OO
.ui
.Keys
.ENTER
) ) {
2338 if ( this.emit( 'click' ) ) {
2345 * Check if button has a frame.
2347 * @return {boolean} Button is framed
2349 OO
.ui
.mixin
.ButtonElement
.prototype.isFramed = function () {
2354 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
2356 * @param {boolean} [framed] Make button framed, omit to toggle
2358 * @return {OO.ui.Element} The element, for chaining
2360 OO
.ui
.mixin
.ButtonElement
.prototype.toggleFramed = function ( framed
) {
2361 framed
= framed
=== undefined ? !this.framed
: !!framed
;
2362 if ( framed
!== this.framed
) {
2363 this.framed
= framed
;
2365 .toggleClass( 'oo-ui-buttonElement-frameless', !framed
)
2366 .toggleClass( 'oo-ui-buttonElement-framed', framed
);
2367 this.updateThemeClasses();
2374 * Set the button's active state.
2376 * The active state can be set on:
2378 * - {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} when it is selected
2379 * - {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} when it is toggle on
2380 * - {@link OO.ui.ButtonWidget ButtonWidget} when clicking the button would only refresh the page
2383 * @param {boolean} value Make button active
2385 * @return {OO.ui.Element} The element, for chaining
2387 OO
.ui
.mixin
.ButtonElement
.prototype.setActive = function ( value
) {
2388 this.active
= !!value
;
2389 this.$element
.toggleClass( 'oo-ui-buttonElement-active', this.active
);
2390 this.updateThemeClasses();
2395 * Check if the button is active
2398 * @return {boolean} The button is active
2400 OO
.ui
.mixin
.ButtonElement
.prototype.isActive = function () {
2405 * Any OOUI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
2406 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
2407 * items from the group is done through the interface the class provides.
2408 * For more information, please see the [OOUI documentation on MediaWiki] [1].
2410 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Groups
2413 * @mixins OO.EmitterList
2417 * @param {Object} [config] Configuration options
2418 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
2419 * is omitted, the group element will use a generated `<div>`.
2421 OO
.ui
.mixin
.GroupElement
= function OoUiMixinGroupElement( config
) {
2422 // Configuration initialization
2423 config
= config
|| {};
2425 // Mixin constructors
2426 OO
.EmitterList
.call( this, config
);
2432 this.setGroupElement( config
.$group
|| $( '<div>' ) );
2437 OO
.mixinClass( OO
.ui
.mixin
.GroupElement
, OO
.EmitterList
);
2444 * A change event is emitted when the set of selected items changes.
2446 * @param {OO.ui.Element[]} items Items currently in the group
2452 * Set the group element.
2454 * If an element is already set, items will be moved to the new element.
2456 * @param {jQuery} $group Element to use as group
2458 OO
.ui
.mixin
.GroupElement
.prototype.setGroupElement = function ( $group
) {
2461 this.$group
= $group
;
2462 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2463 this.$group
.append( this.items
[ i
].$element
);
2468 * Find an item by its data.
2470 * Only the first item with matching data will be returned. To return all matching items,
2471 * use the #findItemsFromData method.
2473 * @param {Object} data Item data to search for
2474 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2476 OO
.ui
.mixin
.GroupElement
.prototype.findItemFromData = function ( data
) {
2478 hash
= OO
.getHash( data
);
2480 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2481 item
= this.items
[ i
];
2482 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2491 * Find items by their data.
2493 * All items with matching data will be returned. To return only the first match, use the #findItemFromData method instead.
2495 * @param {Object} data Item data to search for
2496 * @return {OO.ui.Element[]} Items with equivalent data
2498 OO
.ui
.mixin
.GroupElement
.prototype.findItemsFromData = function ( data
) {
2500 hash
= OO
.getHash( data
),
2503 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2504 item
= this.items
[ i
];
2505 if ( hash
=== OO
.getHash( item
.getData() ) ) {
2514 * Add items to the group.
2516 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2517 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2519 * @param {OO.ui.Element[]} items An array of items to add to the group
2520 * @param {number} [index] Index of the insertion point
2522 * @return {OO.ui.Element} The element, for chaining
2524 OO
.ui
.mixin
.GroupElement
.prototype.addItems = function ( items
, index
) {
2526 if ( items
.length
=== 0 ) {
2531 OO
.EmitterList
.prototype.addItems
.call( this, items
, index
);
2533 this.emit( 'change', this.getItems() );
2540 OO
.ui
.mixin
.GroupElement
.prototype.moveItem = function ( items
, newIndex
) {
2541 // insertItemElements expects this.items to not have been modified yet, so call before the mixin
2542 this.insertItemElements( items
, newIndex
);
2545 newIndex
= OO
.EmitterList
.prototype.moveItem
.call( this, items
, newIndex
);
2553 OO
.ui
.mixin
.GroupElement
.prototype.insertItem = function ( item
, index
) {
2554 item
.setElementGroup( this );
2555 this.insertItemElements( item
, index
);
2558 index
= OO
.EmitterList
.prototype.insertItem
.call( this, item
, index
);
2564 * Insert elements into the group
2567 * @param {OO.ui.Element} itemWidget Item to insert
2568 * @param {number} index Insertion index
2570 OO
.ui
.mixin
.GroupElement
.prototype.insertItemElements = function ( itemWidget
, index
) {
2571 if ( index
=== undefined || index
< 0 || index
>= this.items
.length
) {
2572 this.$group
.append( itemWidget
.$element
);
2573 } else if ( index
=== 0 ) {
2574 this.$group
.prepend( itemWidget
.$element
);
2576 this.items
[ index
].$element
.before( itemWidget
.$element
);
2581 * Remove the specified items from a group.
2583 * Removed items are detached (not removed) from the DOM so that they may be reused.
2584 * To remove all items from a group, you may wish to use the #clearItems method instead.
2586 * @param {OO.ui.Element[]} items An array of items to remove
2588 * @return {OO.ui.Element} The element, for chaining
2590 OO
.ui
.mixin
.GroupElement
.prototype.removeItems = function ( items
) {
2591 var i
, len
, item
, index
;
2593 if ( items
.length
=== 0 ) {
2597 // Remove specific items elements
2598 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
2600 index
= this.items
.indexOf( item
);
2601 if ( index
!== -1 ) {
2602 item
.setElementGroup( null );
2603 item
.$element
.detach();
2608 OO
.EmitterList
.prototype.removeItems
.call( this, items
);
2610 this.emit( 'change', this.getItems() );
2615 * Clear all items from the group.
2617 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2618 * To remove only a subset of items from a group, use the #removeItems method.
2621 * @return {OO.ui.Element} The element, for chaining
2623 OO
.ui
.mixin
.GroupElement
.prototype.clearItems = function () {
2626 // Remove all item elements
2627 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
2628 this.items
[ i
].setElementGroup( null );
2629 this.items
[ i
].$element
.detach();
2633 OO
.EmitterList
.prototype.clearItems
.call( this );
2635 this.emit( 'change', this.getItems() );
2640 * LabelElement is often mixed into other classes to generate a label, which
2641 * helps identify the function of an interface element.
2642 * See the [OOUI documentation on MediaWiki] [1] for more information.
2644 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2650 * @param {Object} [config] Configuration options
2651 * @cfg {jQuery} [$label] The label element created by the class. If this
2652 * configuration is omitted, the label element will use a generated `<span>`.
2653 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2654 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2655 * in the future. See the [OOUI documentation on MediaWiki] [2] for examples.
2656 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Labels
2657 * @cfg {boolean} [invisibleLabel] Whether the label should be visually hidden (but still accessible
2658 * to screen-readers).
2660 OO
.ui
.mixin
.LabelElement
= function OoUiMixinLabelElement( config
) {
2661 // Configuration initialization
2662 config
= config
|| {};
2667 this.invisibleLabel
= null;
2670 this.setLabel( config
.label
|| this.constructor.static.label
);
2671 this.setLabelElement( config
.$label
|| $( '<span>' ) );
2672 this.setInvisibleLabel( config
.invisibleLabel
);
2677 OO
.initClass( OO
.ui
.mixin
.LabelElement
);
2682 * @event labelChange
2683 * @param {string} value
2686 /* Static Properties */
2689 * The label text. The label can be specified as a plaintext string, a function that will
2690 * produce a string in the future, or `null` for no label. The static value will
2691 * be overridden if a label is specified with the #label config option.
2695 * @property {string|Function|null}
2697 OO
.ui
.mixin
.LabelElement
.static.label
= null;
2699 /* Static methods */
2702 * Highlight the first occurrence of the query in the given text
2704 * @param {string} text Text
2705 * @param {string} query Query to find
2706 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2707 * @return {jQuery} Text with the first match of the query
2708 * sub-string wrapped in highlighted span
2710 OO
.ui
.mixin
.LabelElement
.static.highlightQuery = function ( text
, query
, compare
) {
2713 $result
= $( '<span>' );
2717 qLen
= query
.length
;
2718 for ( i
= 0; offset
=== -1 && i
<= tLen
- qLen
; i
++ ) {
2719 if ( compare( query
, text
.slice( i
, i
+ qLen
) ) === 0 ) {
2724 offset
= text
.toLowerCase().indexOf( query
.toLowerCase() );
2727 if ( !query
.length
|| offset
=== -1 ) {
2728 $result
.text( text
);
2731 document
.createTextNode( text
.slice( 0, offset
) ),
2733 .addClass( 'oo-ui-labelElement-label-highlight' )
2734 .text( text
.slice( offset
, offset
+ query
.length
) ),
2735 document
.createTextNode( text
.slice( offset
+ query
.length
) )
2738 return $result
.contents();
2744 * Set the label element.
2746 * If an element is already set, it will be cleaned up before setting up the new element.
2748 * @param {jQuery} $label Element to use as label
2750 OO
.ui
.mixin
.LabelElement
.prototype.setLabelElement = function ( $label
) {
2751 if ( this.$label
) {
2752 this.$label
.removeClass( 'oo-ui-labelElement-label' ).empty();
2755 this.$label
= $label
.addClass( 'oo-ui-labelElement-label' );
2756 this.setLabelContent( this.label
);
2762 * An empty string will result in the label being hidden. A string containing only whitespace will
2763 * be converted to a single ` `.
2765 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2766 * text; or null for no label
2768 * @return {OO.ui.Element} The element, for chaining
2770 OO
.ui
.mixin
.LabelElement
.prototype.setLabel = function ( label
) {
2771 label
= typeof label
=== 'function' ? OO
.ui
.resolveMsg( label
) : label
;
2772 label
= ( ( typeof label
=== 'string' || label
instanceof $ ) && label
.length
) || ( label
instanceof OO
.ui
.HtmlSnippet
&& label
.toString().length
) ? label
: null;
2774 if ( this.label
!== label
) {
2775 if ( this.$label
) {
2776 this.setLabelContent( label
);
2779 this.emit( 'labelChange' );
2782 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2788 * Set whether the label should be visually hidden (but still accessible to screen-readers).
2790 * @param {boolean} invisibleLabel
2792 * @return {OO.ui.Element} The element, for chaining
2794 OO
.ui
.mixin
.LabelElement
.prototype.setInvisibleLabel = function ( invisibleLabel
) {
2795 invisibleLabel
= !!invisibleLabel
;
2797 if ( this.invisibleLabel
!== invisibleLabel
) {
2798 this.invisibleLabel
= invisibleLabel
;
2799 this.emit( 'labelChange' );
2802 this.$label
.toggleClass( 'oo-ui-labelElement-invisible', this.invisibleLabel
);
2803 // Pretend that there is no label, a lot of CSS has been written with this assumption
2804 this.$element
.toggleClass( 'oo-ui-labelElement', !!this.label
&& !this.invisibleLabel
);
2810 * Set the label as plain text with a highlighted query
2812 * @param {string} text Text label to set
2813 * @param {string} query Substring of text to highlight
2814 * @param {Function} [compare] Optional string comparator, e.g. Intl.Collator().compare
2816 * @return {OO.ui.Element} The element, for chaining
2818 OO
.ui
.mixin
.LabelElement
.prototype.setHighlightedQuery = function ( text
, query
, compare
) {
2819 return this.setLabel( this.constructor.static.highlightQuery( text
, query
, compare
) );
2825 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2826 * text; or null for no label
2828 OO
.ui
.mixin
.LabelElement
.prototype.getLabel = function () {
2833 * Set the content of the label.
2835 * Do not call this method until after the label element has been set by #setLabelElement.
2838 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2839 * text; or null for no label
2841 OO
.ui
.mixin
.LabelElement
.prototype.setLabelContent = function ( label
) {
2842 if ( typeof label
=== 'string' ) {
2843 if ( label
.match( /^\s*$/ ) ) {
2844 // Convert whitespace only string to a single non-breaking space
2845 this.$label
.html( ' ' );
2847 this.$label
.text( label
);
2849 } else if ( label
instanceof OO
.ui
.HtmlSnippet
) {
2850 this.$label
.html( label
.toString() );
2851 } else if ( label
instanceof $ ) {
2852 this.$label
.empty().append( label
);
2854 this.$label
.empty();
2859 * IconElement is often mixed into other classes to generate an icon.
2860 * Icons are graphics, about the size of normal text. They are used to aid the user
2861 * in locating a control or to convey information in a space-efficient way. See the
2862 * [OOUI documentation on MediaWiki] [1] for a list of icons
2863 * included in the library.
2865 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2871 * @param {Object} [config] Configuration options
2872 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2873 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2874 * the icon element be set to an existing icon instead of the one generated by this class, set a
2875 * value using a jQuery selection. For example:
2877 * // Use a <div> tag instead of a <span>
2878 * $icon: $( '<div>' )
2879 * // Use an existing icon element instead of the one generated by the class
2880 * $icon: this.$element
2881 * // Use an icon element from a child widget
2882 * $icon: this.childwidget.$element
2883 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2884 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2885 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2886 * by the user's language.
2888 * Example of an i18n map:
2890 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2891 * See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
2892 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
2893 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2894 * text. The icon title is displayed when users move the mouse over the icon.
2896 OO
.ui
.mixin
.IconElement
= function OoUiMixinIconElement( config
) {
2897 // Configuration initialization
2898 config
= config
|| {};
2903 this.iconTitle
= null;
2905 // `iconTitle`s are deprecated since 0.30.0
2906 if ( config
.iconTitle
!== undefined ) {
2907 OO
.ui
.warnDeprecation( 'IconElement: Widgets with iconTitle set are deprecated, use title instead. See T76638 for details.' );
2911 this.setIcon( config
.icon
|| this.constructor.static.icon
);
2912 this.setIconTitle( config
.iconTitle
|| this.constructor.static.iconTitle
);
2913 this.setIconElement( config
.$icon
|| $( '<span>' ) );
2918 OO
.initClass( OO
.ui
.mixin
.IconElement
);
2920 /* Static Properties */
2923 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2924 * for i18n purposes and contains a `default` icon name and additional names keyed by
2925 * language code. The `default` name is used when no icon is keyed by the user's language.
2927 * Example of an i18n map:
2929 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2931 * Note: the static property will be overridden if the #icon configuration is used.
2935 * @property {Object|string}
2937 OO
.ui
.mixin
.IconElement
.static.icon
= null;
2940 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2941 * function that returns title text, or `null` for no title.
2943 * The static property will be overridden if the #iconTitle configuration is used.
2947 * @property {string|Function|null}
2949 OO
.ui
.mixin
.IconElement
.static.iconTitle
= null;
2954 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2955 * applies to the specified icon element instead of the one created by the class. If an icon
2956 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2957 * and mixin methods will no longer affect the element.
2959 * @param {jQuery} $icon Element to use as icon
2961 OO
.ui
.mixin
.IconElement
.prototype.setIconElement = function ( $icon
) {
2964 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon
)
2965 .removeAttr( 'title' );
2969 .addClass( 'oo-ui-iconElement-icon' )
2970 .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
)
2971 .toggleClass( 'oo-ui-icon-' + this.icon
, !!this.icon
);
2972 if ( this.iconTitle
!== null ) {
2973 this.$icon
.attr( 'title', this.iconTitle
);
2976 this.updateThemeClasses();
2980 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2981 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2984 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2985 * by language code, or `null` to remove the icon.
2987 * @return {OO.ui.Element} The element, for chaining
2989 OO
.ui
.mixin
.IconElement
.prototype.setIcon = function ( icon
) {
2990 icon
= OO
.isPlainObject( icon
) ? OO
.ui
.getLocalValue( icon
, null, 'default' ) : icon
;
2991 icon
= typeof icon
=== 'string' && icon
.trim().length
? icon
.trim() : null;
2993 if ( this.icon
!== icon
) {
2995 if ( this.icon
!== null ) {
2996 this.$icon
.removeClass( 'oo-ui-icon-' + this.icon
);
2998 if ( icon
!== null ) {
2999 this.$icon
.addClass( 'oo-ui-icon-' + icon
);
3005 this.$element
.toggleClass( 'oo-ui-iconElement', !!this.icon
);
3007 this.$icon
.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon
);
3009 this.updateThemeClasses();
3015 * Set the icon title. Use `null` to remove the title.
3017 * @param {string|Function|null} iconTitle A text string used as the icon title,
3018 * a function that returns title text, or `null` for no title.
3020 * @return {OO.ui.Element} The element, for chaining
3023 OO
.ui
.mixin
.IconElement
.prototype.setIconTitle = function ( iconTitle
) {
3025 ( typeof iconTitle
=== 'function' || ( typeof iconTitle
=== 'string' && iconTitle
.length
) ) ?
3026 OO
.ui
.resolveMsg( iconTitle
) : null;
3028 if ( this.iconTitle
!== iconTitle
) {
3029 this.iconTitle
= iconTitle
;
3031 if ( this.iconTitle
!== null ) {
3032 this.$icon
.attr( 'title', iconTitle
);
3034 this.$icon
.removeAttr( 'title' );
3039 // `setIconTitle is deprecated since 0.30.0
3040 if ( iconTitle
!== null ) {
3041 // Avoid a warning when this is called from the constructor with no iconTitle set
3042 OO
.ui
.warnDeprecation( 'IconElement: setIconTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
3049 * Get the symbolic name of the icon.
3051 * @return {string} Icon name
3053 OO
.ui
.mixin
.IconElement
.prototype.getIcon = function () {
3058 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
3060 * @return {string} Icon title text
3062 OO
.ui
.mixin
.IconElement
.prototype.getIconTitle = function () {
3063 return this.iconTitle
;
3067 * IndicatorElement is often mixed into other classes to generate an indicator.
3068 * Indicators are small graphics that are generally used in two ways:
3070 * - To draw attention to the status of an item. For example, an indicator might be
3071 * used to show that an item in a list has errors that need to be resolved.
3072 * - To clarify the function of a control that acts in an exceptional way (a button
3073 * that opens a menu instead of performing an action directly, for example).
3075 * For a list of indicators included in the library, please see the
3076 * [OOUI documentation on MediaWiki] [1].
3078 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3084 * @param {Object} [config] Configuration options
3085 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
3086 * configuration is omitted, the indicator element will use a generated `<span>`.
3087 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3088 * See the [OOUI documentation on MediaWiki][2] for a list of indicators included
3090 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3091 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
3092 * or a function that returns title text. The indicator title is displayed when users move
3093 * the mouse over the indicator.
3095 OO
.ui
.mixin
.IndicatorElement
= function OoUiMixinIndicatorElement( config
) {
3096 // Configuration initialization
3097 config
= config
|| {};
3100 this.$indicator
= null;
3101 this.indicator
= null;
3102 this.indicatorTitle
= null;
3104 // `indicatorTitle`s are deprecated since 0.30.0
3105 if ( config
.indicatorTitle
!== undefined ) {
3106 OO
.ui
.warnDeprecation( 'IndicatorElement: Widgets with indicatorTitle set are deprecated, use title instead. See T76638 for details.' );
3110 this.setIndicator( config
.indicator
|| this.constructor.static.indicator
);
3111 this.setIndicatorTitle( config
.indicatorTitle
|| this.constructor.static.indicatorTitle
);
3112 this.setIndicatorElement( config
.$indicator
|| $( '<span>' ) );
3117 OO
.initClass( OO
.ui
.mixin
.IndicatorElement
);
3119 /* Static Properties */
3122 * Symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3123 * The static property will be overridden if the #indicator configuration is used.
3127 * @property {string|null}
3129 OO
.ui
.mixin
.IndicatorElement
.static.indicator
= null;
3132 * A text string used as the indicator title, a function that returns title text, or `null`
3133 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
3137 * @property {string|Function|null}
3139 OO
.ui
.mixin
.IndicatorElement
.static.indicatorTitle
= null;
3144 * Set the indicator element.
3146 * If an element is already set, it will be cleaned up before setting up the new element.
3148 * @param {jQuery} $indicator Element to use as indicator
3150 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorElement = function ( $indicator
) {
3151 if ( this.$indicator
) {
3153 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator
)
3154 .removeAttr( 'title' );
3157 this.$indicator
= $indicator
3158 .addClass( 'oo-ui-indicatorElement-indicator' )
3159 .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
)
3160 .toggleClass( 'oo-ui-indicator-' + this.indicator
, !!this.indicator
);
3161 if ( this.indicatorTitle
!== null ) {
3162 this.$indicator
.attr( 'title', this.indicatorTitle
);
3165 this.updateThemeClasses();
3169 * Set the indicator by its symbolic name: ‘clear’, ‘down’, ‘required’, ‘search’, ‘up’. Use `null` to remove the indicator.
3171 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
3173 * @return {OO.ui.Element} The element, for chaining
3175 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicator = function ( indicator
) {
3176 indicator
= typeof indicator
=== 'string' && indicator
.length
? indicator
.trim() : null;
3178 if ( this.indicator
!== indicator
) {
3179 if ( this.$indicator
) {
3180 if ( this.indicator
!== null ) {
3181 this.$indicator
.removeClass( 'oo-ui-indicator-' + this.indicator
);
3183 if ( indicator
!== null ) {
3184 this.$indicator
.addClass( 'oo-ui-indicator-' + indicator
);
3187 this.indicator
= indicator
;
3190 this.$element
.toggleClass( 'oo-ui-indicatorElement', !!this.indicator
);
3191 if ( this.$indicator
) {
3192 this.$indicator
.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator
);
3194 this.updateThemeClasses();
3200 * Set the indicator title.
3202 * The title is displayed when a user moves the mouse over the indicator.
3204 * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text, or
3205 * `null` for no indicator title
3207 * @return {OO.ui.Element} The element, for chaining
3210 OO
.ui
.mixin
.IndicatorElement
.prototype.setIndicatorTitle = function ( indicatorTitle
) {
3212 ( typeof indicatorTitle
=== 'function' || ( typeof indicatorTitle
=== 'string' && indicatorTitle
.length
) ) ?
3213 OO
.ui
.resolveMsg( indicatorTitle
) : null;
3215 if ( this.indicatorTitle
!== indicatorTitle
) {
3216 this.indicatorTitle
= indicatorTitle
;
3217 if ( this.$indicator
) {
3218 if ( this.indicatorTitle
!== null ) {
3219 this.$indicator
.attr( 'title', indicatorTitle
);
3221 this.$indicator
.removeAttr( 'title' );
3226 // `setIndicatorTitle is deprecated since 0.30.0
3227 if ( indicatorTitle
!== null ) {
3228 // Avoid a warning when this is called from the constructor with no indicatorTitle set
3229 OO
.ui
.warnDeprecation( 'IndicatorElement: setIndicatorTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
3236 * Get the symbolic name of the indicator (e.g., ‘clear’ or ‘down’).
3238 * @return {string} Symbolic name of indicator
3240 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicator = function () {
3241 return this.indicator
;
3245 * Get the indicator title.
3247 * The title is displayed when a user moves the mouse over the indicator.
3249 * @return {string} Indicator title text
3251 OO
.ui
.mixin
.IndicatorElement
.prototype.getIndicatorTitle = function () {
3252 return this.indicatorTitle
;
3256 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
3257 * additional functionality to an element created by another class. The class provides
3258 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
3259 * which are used to customize the look and feel of a widget to better describe its
3260 * importance and functionality.
3262 * The library currently contains the following styling flags for general use:
3264 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
3265 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
3267 * The flags affect the appearance of the buttons:
3270 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
3271 * var button1 = new OO.ui.ButtonWidget( {
3272 * label: 'Progressive',
3273 * flags: 'progressive'
3275 * button2 = new OO.ui.ButtonWidget( {
3276 * label: 'Destructive',
3277 * flags: 'destructive'
3279 * $( document.body ).append( button1.$element, button2.$element );
3281 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
3282 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
3284 * [1]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3290 * @param {Object} [config] Configuration options
3291 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'progressive' or 'primary') to apply.
3292 * Please see the [OOUI documentation on MediaWiki] [2] for more information about available flags.
3293 * [2]: https://www.mediawiki.org/wiki/OOUI/Elements/Flagged
3294 * @cfg {jQuery} [$flagged] The flagged element. By default,
3295 * the flagged functionality is applied to the element created by the class ($element).
3296 * If a different element is specified, the flagged functionality will be applied to it instead.
3298 OO
.ui
.mixin
.FlaggedElement
= function OoUiMixinFlaggedElement( config
) {
3299 // Configuration initialization
3300 config
= config
|| {};
3304 this.$flagged
= null;
3307 this.setFlags( config
.flags
);
3308 this.setFlaggedElement( config
.$flagged
|| this.$element
);
3315 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
3316 * parameter contains the name of each modified flag and indicates whether it was
3319 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
3320 * that the flag was added, `false` that the flag was removed.
3326 * Set the flagged element.
3328 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
3329 * If an element is already set, the method will remove the mixin’s effect on that element.
3331 * @param {jQuery} $flagged Element that should be flagged
3333 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlaggedElement = function ( $flagged
) {
3334 var classNames
= Object
.keys( this.flags
).map( function ( flag
) {
3335 return 'oo-ui-flaggedElement-' + flag
;
3338 if ( this.$flagged
) {
3339 this.$flagged
.removeClass( classNames
);
3342 this.$flagged
= $flagged
.addClass( classNames
);
3346 * Check if the specified flag is set.
3348 * @param {string} flag Name of flag
3349 * @return {boolean} The flag is set
3351 OO
.ui
.mixin
.FlaggedElement
.prototype.hasFlag = function ( flag
) {
3352 // This may be called before the constructor, thus before this.flags is set
3353 return this.flags
&& ( flag
in this.flags
);
3357 * Get the names of all flags set.
3359 * @return {string[]} Flag names
3361 OO
.ui
.mixin
.FlaggedElement
.prototype.getFlags = function () {
3362 // This may be called before the constructor, thus before this.flags is set
3363 return Object
.keys( this.flags
|| {} );
3370 * @return {OO.ui.Element} The element, for chaining
3373 OO
.ui
.mixin
.FlaggedElement
.prototype.clearFlags = function () {
3374 var flag
, className
,
3377 classPrefix
= 'oo-ui-flaggedElement-';
3379 for ( flag
in this.flags
) {
3380 className
= classPrefix
+ flag
;
3381 changes
[ flag
] = false;
3382 delete this.flags
[ flag
];
3383 remove
.push( className
);
3386 if ( this.$flagged
) {
3387 this.$flagged
.removeClass( remove
);
3390 this.updateThemeClasses();
3391 this.emit( 'flag', changes
);
3397 * Add one or more flags.
3399 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
3400 * or an object keyed by flag name with a boolean value that indicates whether the flag should
3401 * be added (`true`) or removed (`false`).
3403 * @return {OO.ui.Element} The element, for chaining
3406 OO
.ui
.mixin
.FlaggedElement
.prototype.setFlags = function ( flags
) {
3407 var i
, len
, flag
, className
,
3411 classPrefix
= 'oo-ui-flaggedElement-';
3413 if ( typeof flags
=== 'string' ) {
3414 className
= classPrefix
+ flags
;
3416 if ( !this.flags
[ flags
] ) {
3417 this.flags
[ flags
] = true;
3418 add
.push( className
);
3420 } else if ( Array
.isArray( flags
) ) {
3421 for ( i
= 0, len
= flags
.length
; i
< len
; i
++ ) {
3423 className
= classPrefix
+ flag
;
3425 if ( !this.flags
[ flag
] ) {
3426 changes
[ flag
] = true;
3427 this.flags
[ flag
] = true;
3428 add
.push( className
);
3431 } else if ( OO
.isPlainObject( flags
) ) {
3432 for ( flag
in flags
) {
3433 className
= classPrefix
+ flag
;
3434 if ( flags
[ flag
] ) {
3436 if ( !this.flags
[ flag
] ) {
3437 changes
[ flag
] = true;
3438 this.flags
[ flag
] = true;
3439 add
.push( className
);
3443 if ( this.flags
[ flag
] ) {
3444 changes
[ flag
] = false;
3445 delete this.flags
[ flag
];
3446 remove
.push( className
);
3452 if ( this.$flagged
) {
3455 .removeClass( remove
);
3458 this.updateThemeClasses();
3459 this.emit( 'flag', changes
);
3465 * TitledElement is mixed into other classes to provide a `title` attribute.
3466 * Titles are rendered by the browser and are made visible when the user moves
3467 * the mouse over the element. Titles are not visible on touch devices.
3470 * // TitledElement provides a `title` attribute to the
3471 * // ButtonWidget class.
3472 * var button = new OO.ui.ButtonWidget( {
3473 * label: 'Button with Title',
3474 * title: 'I am a button'
3476 * $( document.body ).append( button.$element );
3482 * @param {Object} [config] Configuration options
3483 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
3484 * If this config is omitted, the title functionality is applied to $element, the
3485 * element created by the class.
3486 * @cfg {string|Function} [title] The title text or a function that returns text. If
3487 * this config is omitted, the value of the {@link #static-title static title} property is used.
3489 OO
.ui
.mixin
.TitledElement
= function OoUiMixinTitledElement( config
) {
3490 // Configuration initialization
3491 config
= config
|| {};
3494 this.$titled
= null;
3498 this.setTitle( config
.title
!== undefined ? config
.title
: this.constructor.static.title
);
3499 this.setTitledElement( config
.$titled
|| this.$element
);
3504 OO
.initClass( OO
.ui
.mixin
.TitledElement
);
3506 /* Static Properties */
3509 * The title text, a function that returns text, or `null` for no title. The value of the static property
3510 * is overridden if the #title config option is used.
3514 * @property {string|Function|null}
3516 OO
.ui
.mixin
.TitledElement
.static.title
= null;
3521 * Set the titled element.
3523 * This method is used to retarget a TitledElement mixin so that its functionality applies to the specified element.
3524 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3526 * @param {jQuery} $titled Element that should use the 'titled' functionality
3528 OO
.ui
.mixin
.TitledElement
.prototype.setTitledElement = function ( $titled
) {
3529 if ( this.$titled
) {
3530 this.$titled
.removeAttr( 'title' );
3533 this.$titled
= $titled
;
3542 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3544 * @return {OO.ui.Element} The element, for chaining
3546 OO
.ui
.mixin
.TitledElement
.prototype.setTitle = function ( title
) {
3547 title
= typeof title
=== 'function' ? OO
.ui
.resolveMsg( title
) : title
;
3548 title
= ( typeof title
=== 'string' && title
.length
) ? title
: null;
3550 if ( this.title
!== title
) {
3559 * Update the title attribute, in case of changes to title or accessKey.
3563 * @return {OO.ui.Element} The element, for chaining
3565 OO
.ui
.mixin
.TitledElement
.prototype.updateTitle = function () {
3566 var title
= this.getTitle();
3567 if ( this.$titled
) {
3568 if ( title
!== null ) {
3569 // Only if this is an AccessKeyedElement
3570 if ( this.formatTitleWithAccessKey
) {
3571 title
= this.formatTitleWithAccessKey( title
);
3573 this.$titled
.attr( 'title', title
);
3575 this.$titled
.removeAttr( 'title' );
3584 * @return {string} Title string
3586 OO
.ui
.mixin
.TitledElement
.prototype.getTitle = function () {
3591 * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
3592 * Accesskeys allow an user to go to a specific element by using
3593 * a shortcut combination of a browser specific keys + the key
3597 * // AccessKeyedElement provides an `accesskey` attribute to the
3598 * // ButtonWidget class.
3599 * var button = new OO.ui.ButtonWidget( {
3600 * label: 'Button with Accesskey',
3603 * $( document.body ).append( button.$element );
3609 * @param {Object} [config] Configuration options
3610 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3611 * If this config is omitted, the accesskey functionality is applied to $element, the
3612 * element created by the class.
3613 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3614 * this config is omitted, no accesskey will be added.
3616 OO
.ui
.mixin
.AccessKeyedElement
= function OoUiMixinAccessKeyedElement( config
) {
3617 // Configuration initialization
3618 config
= config
|| {};
3621 this.$accessKeyed
= null;
3622 this.accessKey
= null;
3625 this.setAccessKey( config
.accessKey
|| null );
3626 this.setAccessKeyedElement( config
.$accessKeyed
|| this.$element
);
3628 // If this is also a TitledElement and it initialized before we did, we may have
3629 // to update the title with the access key
3630 if ( this.updateTitle
) {
3637 OO
.initClass( OO
.ui
.mixin
.AccessKeyedElement
);
3639 /* Static Properties */
3642 * The access key, a function that returns a key, or `null` for no accesskey.
3646 * @property {string|Function|null}
3648 OO
.ui
.mixin
.AccessKeyedElement
.static.accessKey
= null;
3653 * Set the accesskeyed element.
3655 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3656 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3658 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyed' functionality
3660 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKeyedElement = function ( $accessKeyed
) {
3661 if ( this.$accessKeyed
) {
3662 this.$accessKeyed
.removeAttr( 'accesskey' );
3665 this.$accessKeyed
= $accessKeyed
;
3666 if ( this.accessKey
) {
3667 this.$accessKeyed
.attr( 'accesskey', this.accessKey
);
3674 * @param {string|Function|null} accessKey Key, a function that returns a key, or `null` for no accesskey
3676 * @return {OO.ui.Element} The element, for chaining
3678 OO
.ui
.mixin
.AccessKeyedElement
.prototype.setAccessKey = function ( accessKey
) {
3679 accessKey
= typeof accessKey
=== 'string' ? OO
.ui
.resolveMsg( accessKey
) : null;
3681 if ( this.accessKey
!== accessKey
) {
3682 if ( this.$accessKeyed
) {
3683 if ( accessKey
!== null ) {
3684 this.$accessKeyed
.attr( 'accesskey', accessKey
);
3686 this.$accessKeyed
.removeAttr( 'accesskey' );
3689 this.accessKey
= accessKey
;
3691 // Only if this is a TitledElement
3692 if ( this.updateTitle
) {
3703 * @return {string} accessKey string
3705 OO
.ui
.mixin
.AccessKeyedElement
.prototype.getAccessKey = function () {
3706 return this.accessKey
;
3710 * Add information about the access key to the element's tooltip label.
3711 * (This is only public for hacky usage in FieldLayout.)
3713 * @param {string} title Tooltip label for `title` attribute
3716 OO
.ui
.mixin
.AccessKeyedElement
.prototype.formatTitleWithAccessKey = function ( title
) {
3719 if ( !this.$accessKeyed
) {
3720 // Not initialized yet; the constructor will call updateTitle() which will rerun this function
3723 // Use jquery.accessKeyLabel if available to show modifiers, otherwise just display the single key
3724 if ( $.fn
.updateTooltipAccessKeys
&& $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel
) {
3725 accessKey
= $.fn
.updateTooltipAccessKeys
.getAccessKeyLabel( this.$accessKeyed
[ 0 ] );
3727 accessKey
= this.getAccessKey();
3730 title
+= ' [' + accessKey
+ ']';
3736 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3737 * feels, and functionality can be customized via the class’s configuration options
3738 * and methods. Please see the [OOUI documentation on MediaWiki] [1] for more information
3741 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
3744 * // A button widget.
3745 * var button = new OO.ui.ButtonWidget( {
3746 * label: 'Button with Icon',
3750 * $( document.body ).append( button.$element );
3752 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3755 * @extends OO.ui.Widget
3756 * @mixins OO.ui.mixin.ButtonElement
3757 * @mixins OO.ui.mixin.IconElement
3758 * @mixins OO.ui.mixin.IndicatorElement
3759 * @mixins OO.ui.mixin.LabelElement
3760 * @mixins OO.ui.mixin.TitledElement
3761 * @mixins OO.ui.mixin.FlaggedElement
3762 * @mixins OO.ui.mixin.TabIndexedElement
3763 * @mixins OO.ui.mixin.AccessKeyedElement
3766 * @param {Object} [config] Configuration options
3767 * @cfg {boolean} [active=false] Whether button should be shown as active
3768 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3769 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3770 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3772 OO
.ui
.ButtonWidget
= function OoUiButtonWidget( config
) {
3773 // Configuration initialization
3774 config
= config
|| {};
3776 // Parent constructor
3777 OO
.ui
.ButtonWidget
.parent
.call( this, config
);
3779 // Mixin constructors
3780 OO
.ui
.mixin
.ButtonElement
.call( this, config
);
3781 OO
.ui
.mixin
.IconElement
.call( this, config
);
3782 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
3783 OO
.ui
.mixin
.LabelElement
.call( this, config
);
3784 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$button
} ) );
3785 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
3786 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$button
} ) );
3787 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$button
} ) );
3792 this.noFollow
= false;
3795 this.connect( this, { disable
: 'onDisable' } );
3798 this.$button
.append( this.$icon
, this.$label
, this.$indicator
);
3800 .addClass( 'oo-ui-buttonWidget' )
3801 .append( this.$button
);
3802 this.setActive( config
.active
);
3803 this.setHref( config
.href
);
3804 this.setTarget( config
.target
);
3805 this.setNoFollow( config
.noFollow
);
3810 OO
.inheritClass( OO
.ui
.ButtonWidget
, OO
.ui
.Widget
);
3811 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.ButtonElement
);
3812 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IconElement
);
3813 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.IndicatorElement
);
3814 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.LabelElement
);
3815 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TitledElement
);
3816 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.FlaggedElement
);
3817 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.TabIndexedElement
);
3818 OO
.mixinClass( OO
.ui
.ButtonWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
3820 /* Static Properties */
3826 OO
.ui
.ButtonWidget
.static.cancelButtonMouseDownEvents
= false;
3832 OO
.ui
.ButtonWidget
.static.tagName
= 'span';
3837 * Get hyperlink location.
3839 * @return {string} Hyperlink location
3841 OO
.ui
.ButtonWidget
.prototype.getHref = function () {
3846 * Get hyperlink target.
3848 * @return {string} Hyperlink target
3850 OO
.ui
.ButtonWidget
.prototype.getTarget = function () {
3855 * Get search engine traversal hint.
3857 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3859 OO
.ui
.ButtonWidget
.prototype.getNoFollow = function () {
3860 return this.noFollow
;
3864 * Set hyperlink location.
3866 * @param {string|null} href Hyperlink location, null to remove
3868 * @return {OO.ui.Widget} The widget, for chaining
3870 OO
.ui
.ButtonWidget
.prototype.setHref = function ( href
) {
3871 href
= typeof href
=== 'string' ? href
: null;
3872 if ( href
!== null && !OO
.ui
.isSafeUrl( href
) ) {
3876 if ( href
!== this.href
) {
3885 * Update the `href` attribute, in case of changes to href or
3890 * @return {OO.ui.Widget} The widget, for chaining
3892 OO
.ui
.ButtonWidget
.prototype.updateHref = function () {
3893 if ( this.href
!== null && !this.isDisabled() ) {
3894 this.$button
.attr( 'href', this.href
);
3896 this.$button
.removeAttr( 'href' );
3903 * Handle disable events.
3906 * @param {boolean} disabled Element is disabled
3908 OO
.ui
.ButtonWidget
.prototype.onDisable = function () {
3913 * Set hyperlink target.
3915 * @param {string|null} target Hyperlink target, null to remove
3916 * @return {OO.ui.Widget} The widget, for chaining
3918 OO
.ui
.ButtonWidget
.prototype.setTarget = function ( target
) {
3919 target
= typeof target
=== 'string' ? target
: null;
3921 if ( target
!== this.target
) {
3922 this.target
= target
;
3923 if ( target
!== null ) {
3924 this.$button
.attr( 'target', target
);
3926 this.$button
.removeAttr( 'target' );
3934 * Set search engine traversal hint.
3936 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3937 * @return {OO.ui.Widget} The widget, for chaining
3939 OO
.ui
.ButtonWidget
.prototype.setNoFollow = function ( noFollow
) {
3940 noFollow
= typeof noFollow
=== 'boolean' ? noFollow
: true;
3942 if ( noFollow
!== this.noFollow
) {
3943 this.noFollow
= noFollow
;
3945 this.$button
.attr( 'rel', 'nofollow' );
3947 this.$button
.removeAttr( 'rel' );
3954 // Override method visibility hints from ButtonElement
3965 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3966 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3967 * removed, and cleared from the group.
3970 * // A ButtonGroupWidget with two buttons.
3971 * var button1 = new OO.ui.PopupButtonWidget( {
3972 * label: 'Select a category',
3975 * $content: $( '<p>List of categories…</p>' ),
3980 * button2 = new OO.ui.ButtonWidget( {
3983 * buttonGroup = new OO.ui.ButtonGroupWidget( {
3984 * items: [ button1, button2 ]
3986 * $( document.body ).append( buttonGroup.$element );
3989 * @extends OO.ui.Widget
3990 * @mixins OO.ui.mixin.GroupElement
3991 * @mixins OO.ui.mixin.TitledElement
3994 * @param {Object} [config] Configuration options
3995 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3997 OO
.ui
.ButtonGroupWidget
= function OoUiButtonGroupWidget( config
) {
3998 // Configuration initialization
3999 config
= config
|| {};
4001 // Parent constructor
4002 OO
.ui
.ButtonGroupWidget
.parent
.call( this, config
);
4004 // Mixin constructors
4005 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
4006 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4009 this.$element
.addClass( 'oo-ui-buttonGroupWidget' );
4010 if ( Array
.isArray( config
.items
) ) {
4011 this.addItems( config
.items
);
4017 OO
.inheritClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.Widget
);
4018 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.GroupElement
);
4019 OO
.mixinClass( OO
.ui
.ButtonGroupWidget
, OO
.ui
.mixin
.TitledElement
);
4021 /* Static Properties */
4027 OO
.ui
.ButtonGroupWidget
.static.tagName
= 'span';
4035 * @return {OO.ui.Widget} The widget, for chaining
4037 OO
.ui
.ButtonGroupWidget
.prototype.focus = function () {
4038 if ( !this.isDisabled() ) {
4039 if ( this.items
[ 0 ] ) {
4040 this.items
[ 0 ].focus();
4049 OO
.ui
.ButtonGroupWidget
.prototype.simulateLabelClick = function () {
4054 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
4055 * which creates a label that identifies the icon’s function. See the [OOUI documentation on MediaWiki] [1]
4056 * for a list of icons included in the library.
4059 * // An IconWidget with a label via LabelWidget.
4060 * var myIcon = new OO.ui.IconWidget( {
4064 * // Create a label.
4065 * iconLabel = new OO.ui.LabelWidget( {
4068 * $( document.body ).append( myIcon.$element, iconLabel.$element );
4070 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
4073 * @extends OO.ui.Widget
4074 * @mixins OO.ui.mixin.IconElement
4075 * @mixins OO.ui.mixin.TitledElement
4076 * @mixins OO.ui.mixin.LabelElement
4077 * @mixins OO.ui.mixin.FlaggedElement
4080 * @param {Object} [config] Configuration options
4082 OO
.ui
.IconWidget
= function OoUiIconWidget( config
) {
4083 // Configuration initialization
4084 config
= config
|| {};
4086 // Parent constructor
4087 OO
.ui
.IconWidget
.parent
.call( this, config
);
4089 // Mixin constructors
4090 OO
.ui
.mixin
.IconElement
.call( this, $.extend( {}, config
, { $icon
: this.$element
} ) );
4091 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
4092 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
, invisibleLabel
: true } ) );
4093 OO
.ui
.mixin
.FlaggedElement
.call( this, $.extend( {}, config
, { $flagged
: this.$element
} ) );
4096 this.$element
.addClass( 'oo-ui-iconWidget' );
4097 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4098 // nested in other widgets, because this widget used to not mix in LabelElement.
4099 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4104 OO
.inheritClass( OO
.ui
.IconWidget
, OO
.ui
.Widget
);
4105 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.IconElement
);
4106 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.TitledElement
);
4107 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.LabelElement
);
4108 OO
.mixinClass( OO
.ui
.IconWidget
, OO
.ui
.mixin
.FlaggedElement
);
4110 /* Static Properties */
4116 OO
.ui
.IconWidget
.static.tagName
= 'span';
4119 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
4120 * attention to the status of an item or to clarify the function within a control. For a list of
4121 * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
4124 * // An indicator widget.
4125 * var indicator1 = new OO.ui.IndicatorWidget( {
4126 * indicator: 'required'
4128 * // Create a fieldset layout to add a label.
4129 * fieldset = new OO.ui.FieldsetLayout();
4130 * fieldset.addItems( [
4131 * new OO.ui.FieldLayout( indicator1, {
4132 * label: 'A required indicator:'
4135 * $( document.body ).append( fieldset.$element );
4137 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4140 * @extends OO.ui.Widget
4141 * @mixins OO.ui.mixin.IndicatorElement
4142 * @mixins OO.ui.mixin.TitledElement
4143 * @mixins OO.ui.mixin.LabelElement
4146 * @param {Object} [config] Configuration options
4148 OO
.ui
.IndicatorWidget
= function OoUiIndicatorWidget( config
) {
4149 // Configuration initialization
4150 config
= config
|| {};
4152 // Parent constructor
4153 OO
.ui
.IndicatorWidget
.parent
.call( this, config
);
4155 // Mixin constructors
4156 OO
.ui
.mixin
.IndicatorElement
.call( this, $.extend( {}, config
, { $indicator
: this.$element
} ) );
4157 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$element
} ) );
4158 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
, invisibleLabel
: true } ) );
4161 this.$element
.addClass( 'oo-ui-indicatorWidget' );
4162 // Remove class added by LabelElement initialization. It causes unexpected CSS to apply when
4163 // nested in other widgets, because this widget used to not mix in LabelElement.
4164 this.$element
.removeClass( 'oo-ui-labelElement-label' );
4169 OO
.inheritClass( OO
.ui
.IndicatorWidget
, OO
.ui
.Widget
);
4170 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.IndicatorElement
);
4171 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.TitledElement
);
4172 OO
.mixinClass( OO
.ui
.IndicatorWidget
, OO
.ui
.mixin
.LabelElement
);
4174 /* Static Properties */
4180 OO
.ui
.IndicatorWidget
.static.tagName
= 'span';
4183 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
4184 * be configured with a `label` option that is set to a string, a label node, or a function:
4186 * - String: a plaintext string
4187 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
4188 * label that includes a link or special styling, such as a gray color or additional graphical elements.
4189 * - Function: a function that will produce a string in the future. Functions are used
4190 * in cases where the value of the label is not currently defined.
4192 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
4193 * will come into focus when the label is clicked.
4196 * // Two LabelWidgets.
4197 * var label1 = new OO.ui.LabelWidget( {
4198 * label: 'plaintext label'
4200 * label2 = new OO.ui.LabelWidget( {
4201 * label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
4203 * // Create a fieldset layout with fields for each example.
4204 * fieldset = new OO.ui.FieldsetLayout();
4205 * fieldset.addItems( [
4206 * new OO.ui.FieldLayout( label1 ),
4207 * new OO.ui.FieldLayout( label2 )
4209 * $( document.body ).append( fieldset.$element );
4212 * @extends OO.ui.Widget
4213 * @mixins OO.ui.mixin.LabelElement
4214 * @mixins OO.ui.mixin.TitledElement
4217 * @param {Object} [config] Configuration options
4218 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
4219 * Clicking the label will focus the specified input field.
4221 OO
.ui
.LabelWidget
= function OoUiLabelWidget( config
) {
4222 // Configuration initialization
4223 config
= config
|| {};
4225 // Parent constructor
4226 OO
.ui
.LabelWidget
.parent
.call( this, config
);
4228 // Mixin constructors
4229 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, { $label
: this.$element
} ) );
4230 OO
.ui
.mixin
.TitledElement
.call( this, config
);
4233 this.input
= config
.input
;
4237 if ( this.input
.getInputId() ) {
4238 this.$element
.attr( 'for', this.input
.getInputId() );
4240 this.$label
.on( 'click', function () {
4241 this.input
.simulateLabelClick();
4245 this.$element
.addClass( 'oo-ui-labelWidget' );
4250 OO
.inheritClass( OO
.ui
.LabelWidget
, OO
.ui
.Widget
);
4251 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.LabelElement
);
4252 OO
.mixinClass( OO
.ui
.LabelWidget
, OO
.ui
.mixin
.TitledElement
);
4254 /* Static Properties */
4260 OO
.ui
.LabelWidget
.static.tagName
= 'label';
4263 * PendingElement is a mixin that is used to create elements that notify users that something is happening
4264 * and that they should wait before proceeding. The pending state is visually represented with a pending
4265 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
4266 * field of a {@link OO.ui.TextInputWidget text input widget}.
4268 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
4269 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
4270 * in process dialogs.
4273 * function MessageDialog( config ) {
4274 * MessageDialog.parent.call( this, config );
4276 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
4278 * MessageDialog.static.name = 'myMessageDialog';
4279 * MessageDialog.static.actions = [
4280 * { action: 'save', label: 'Done', flags: 'primary' },
4281 * { label: 'Cancel', flags: 'safe' }
4284 * MessageDialog.prototype.initialize = function () {
4285 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
4286 * this.content = new OO.ui.PanelLayout( { padded: true } );
4287 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.</p>' );
4288 * this.$body.append( this.content.$element );
4290 * MessageDialog.prototype.getBodyHeight = function () {
4293 * MessageDialog.prototype.getActionProcess = function ( action ) {
4294 * var dialog = this;
4295 * if ( action === 'save' ) {
4296 * dialog.getActions().get({actions: 'save'})[0].pushPending();
4297 * return new OO.ui.Process()
4299 * .next( function () {
4300 * dialog.getActions().get({actions: 'save'})[0].popPending();
4303 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
4306 * var windowManager = new OO.ui.WindowManager();
4307 * $( document.body ).append( windowManager.$element );
4309 * var dialog = new MessageDialog();
4310 * windowManager.addWindows( [ dialog ] );
4311 * windowManager.openWindow( dialog );
4317 * @param {Object} [config] Configuration options
4318 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
4320 OO
.ui
.mixin
.PendingElement
= function OoUiMixinPendingElement( config
) {
4321 // Configuration initialization
4322 config
= config
|| {};
4326 this.$pending
= null;
4329 this.setPendingElement( config
.$pending
|| this.$element
);
4334 OO
.initClass( OO
.ui
.mixin
.PendingElement
);
4339 * Set the pending element (and clean up any existing one).
4341 * @param {jQuery} $pending The element to set to pending.
4343 OO
.ui
.mixin
.PendingElement
.prototype.setPendingElement = function ( $pending
) {
4344 if ( this.$pending
) {
4345 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4348 this.$pending
= $pending
;
4349 if ( this.pending
> 0 ) {
4350 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4355 * Check if an element is pending.
4357 * @return {boolean} Element is pending
4359 OO
.ui
.mixin
.PendingElement
.prototype.isPending = function () {
4360 return !!this.pending
;
4364 * Increase the pending counter. The pending state will remain active until the counter is zero
4365 * (i.e., the number of calls to #pushPending and #popPending is the same).
4368 * @return {OO.ui.Element} The element, for chaining
4370 OO
.ui
.mixin
.PendingElement
.prototype.pushPending = function () {
4371 if ( this.pending
=== 0 ) {
4372 this.$pending
.addClass( 'oo-ui-pendingElement-pending' );
4373 this.updateThemeClasses();
4381 * Decrease the pending counter. The pending state will remain active until the counter is zero
4382 * (i.e., the number of calls to #pushPending and #popPending is the same).
4385 * @return {OO.ui.Element} The element, for chaining
4387 OO
.ui
.mixin
.PendingElement
.prototype.popPending = function () {
4388 if ( this.pending
=== 1 ) {
4389 this.$pending
.removeClass( 'oo-ui-pendingElement-pending' );
4390 this.updateThemeClasses();
4392 this.pending
= Math
.max( 0, this.pending
- 1 );
4398 * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
4399 * in the document (for example, in an OO.ui.Window's $overlay).
4401 * The elements's position is automatically calculated and maintained when window is resized or the
4402 * page is scrolled. If you reposition the container manually, you have to call #position to make
4403 * sure the element is still placed correctly.
4405 * As positioning is only possible when both the element and the container are attached to the DOM
4406 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
4407 * the #toggle method to display a floating popup, for example.
4413 * @param {Object} [config] Configuration options
4414 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
4415 * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
4416 * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
4417 * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
4418 * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
4419 * 'top': Align the top edge with $floatableContainer's top edge
4420 * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
4421 * 'center': Vertically align the center with $floatableContainer's center
4422 * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
4423 * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
4424 * 'after': Directly after $floatableContainer, aligning f's start edge with fC's end edge
4425 * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
4426 * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
4427 * 'center': Horizontally align the center with $floatableContainer's center
4428 * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
4431 OO
.ui
.mixin
.FloatableElement
= function OoUiMixinFloatableElement( config
) {
4432 // Configuration initialization
4433 config
= config
|| {};
4436 this.$floatable
= null;
4437 this.$floatableContainer
= null;
4438 this.$floatableWindow
= null;
4439 this.$floatableClosestScrollable
= null;
4440 this.floatableOutOfView
= false;
4441 this.onFloatableScrollHandler
= this.position
.bind( this );
4442 this.onFloatableWindowResizeHandler
= this.position
.bind( this );
4445 this.setFloatableContainer( config
.$floatableContainer
);
4446 this.setFloatableElement( config
.$floatable
|| this.$element
);
4447 this.setVerticalPosition( config
.verticalPosition
|| 'below' );
4448 this.setHorizontalPosition( config
.horizontalPosition
|| 'start' );
4449 this.hideWhenOutOfView
= config
.hideWhenOutOfView
=== undefined ? true : !!config
.hideWhenOutOfView
;
4455 * Set floatable element.
4457 * If an element is already set, it will be cleaned up before setting up the new element.
4459 * @param {jQuery} $floatable Element to make floatable
4461 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableElement = function ( $floatable
) {
4462 if ( this.$floatable
) {
4463 this.$floatable
.removeClass( 'oo-ui-floatableElement-floatable' );
4464 this.$floatable
.css( { left
: '', top
: '' } );
4467 this.$floatable
= $floatable
.addClass( 'oo-ui-floatableElement-floatable' );
4472 * Set floatable container.
4474 * The element will be positioned relative to the specified container.
4476 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
4478 OO
.ui
.mixin
.FloatableElement
.prototype.setFloatableContainer = function ( $floatableContainer
) {
4479 this.$floatableContainer
= $floatableContainer
;
4480 if ( this.$floatable
) {
4486 * Change how the element is positioned vertically.
4488 * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
4490 OO
.ui
.mixin
.FloatableElement
.prototype.setVerticalPosition = function ( position
) {
4491 if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position
) === -1 ) {
4492 throw new Error( 'Invalid value for vertical position: ' + position
);
4494 if ( this.verticalPosition
!== position
) {
4495 this.verticalPosition
= position
;
4496 if ( this.$floatable
) {
4503 * Change how the element is positioned horizontally.
4505 * @param {string} position 'before', 'after', 'start', 'end' or 'center'
4507 OO
.ui
.mixin
.FloatableElement
.prototype.setHorizontalPosition = function ( position
) {
4508 if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position
) === -1 ) {
4509 throw new Error( 'Invalid value for horizontal position: ' + position
);
4511 if ( this.horizontalPosition
!== position
) {
4512 this.horizontalPosition
= position
;
4513 if ( this.$floatable
) {
4520 * Toggle positioning.
4522 * Do not turn positioning on until after the element is attached to the DOM and visible.
4524 * @param {boolean} [positioning] Enable positioning, omit to toggle
4526 * @return {OO.ui.Element} The element, for chaining
4528 OO
.ui
.mixin
.FloatableElement
.prototype.togglePositioning = function ( positioning
) {
4529 var closestScrollableOfContainer
;
4531 if ( !this.$floatable
|| !this.$floatableContainer
) {
4535 positioning
= positioning
=== undefined ? !this.positioning
: !!positioning
;
4537 if ( positioning
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4538 OO
.ui
.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
4539 this.warnedUnattached
= true;
4542 if ( this.positioning
!== positioning
) {
4543 this.positioning
= positioning
;
4545 closestScrollableOfContainer
= OO
.ui
.Element
.static.getClosestScrollableContainer( this.$floatableContainer
[ 0 ] );
4546 // If the scrollable is the root, we have to listen to scroll events
4547 // on the window because of browser inconsistencies.
4548 if ( $( closestScrollableOfContainer
).is( 'html, body' ) ) {
4549 closestScrollableOfContainer
= OO
.ui
.Element
.static.getWindow( closestScrollableOfContainer
);
4552 if ( positioning
) {
4553 this.$floatableWindow
= $( this.getElementWindow() );
4554 this.$floatableWindow
.on( 'resize', this.onFloatableWindowResizeHandler
);
4556 this.$floatableClosestScrollable
= $( closestScrollableOfContainer
);
4557 this.$floatableClosestScrollable
.on( 'scroll', this.onFloatableScrollHandler
);
4559 // Initial position after visible
4562 if ( this.$floatableWindow
) {
4563 this.$floatableWindow
.off( 'resize', this.onFloatableWindowResizeHandler
);
4564 this.$floatableWindow
= null;
4567 if ( this.$floatableClosestScrollable
) {
4568 this.$floatableClosestScrollable
.off( 'scroll', this.onFloatableScrollHandler
);
4569 this.$floatableClosestScrollable
= null;
4572 this.$floatable
.css( { left
: '', right
: '', top
: '' } );
4580 * Check whether the bottom edge of the given element is within the viewport of the given container.
4583 * @param {jQuery} $element
4584 * @param {jQuery} $container
4587 OO
.ui
.mixin
.FloatableElement
.prototype.isElementInViewport = function ( $element
, $container
) {
4588 var elemRect
, contRect
, topEdgeInBounds
, bottomEdgeInBounds
, leftEdgeInBounds
, rightEdgeInBounds
,
4589 startEdgeInBounds
, endEdgeInBounds
, viewportSpacing
,
4590 direction
= $element
.css( 'direction' );
4592 elemRect
= $element
[ 0 ].getBoundingClientRect();
4593 if ( $container
[ 0 ] === window
) {
4594 viewportSpacing
= OO
.ui
.getViewportSpacing();
4598 right
: document
.documentElement
.clientWidth
,
4599 bottom
: document
.documentElement
.clientHeight
4601 contRect
.top
+= viewportSpacing
.top
;
4602 contRect
.left
+= viewportSpacing
.left
;
4603 contRect
.right
-= viewportSpacing
.right
;
4604 contRect
.bottom
-= viewportSpacing
.bottom
;
4606 contRect
= $container
[ 0 ].getBoundingClientRect();
4609 topEdgeInBounds
= elemRect
.top
>= contRect
.top
&& elemRect
.top
<= contRect
.bottom
;
4610 bottomEdgeInBounds
= elemRect
.bottom
>= contRect
.top
&& elemRect
.bottom
<= contRect
.bottom
;
4611 leftEdgeInBounds
= elemRect
.left
>= contRect
.left
&& elemRect
.left
<= contRect
.right
;
4612 rightEdgeInBounds
= elemRect
.right
>= contRect
.left
&& elemRect
.right
<= contRect
.right
;
4613 if ( direction
=== 'rtl' ) {
4614 startEdgeInBounds
= rightEdgeInBounds
;
4615 endEdgeInBounds
= leftEdgeInBounds
;
4617 startEdgeInBounds
= leftEdgeInBounds
;
4618 endEdgeInBounds
= rightEdgeInBounds
;
4621 if ( this.verticalPosition
=== 'below' && !bottomEdgeInBounds
) {
4624 if ( this.verticalPosition
=== 'above' && !topEdgeInBounds
) {
4627 if ( this.horizontalPosition
=== 'before' && !startEdgeInBounds
) {
4630 if ( this.horizontalPosition
=== 'after' && !endEdgeInBounds
) {
4634 // The other positioning values are all about being inside the container,
4635 // so in those cases all we care about is that any part of the container is visible.
4636 return elemRect
.top
<= contRect
.bottom
&& elemRect
.bottom
>= contRect
.top
&&
4637 elemRect
.left
<= contRect
.right
&& elemRect
.right
>= contRect
.left
;
4641 * Check if the floatable is hidden to the user because it was offscreen.
4643 * @return {boolean} Floatable is out of view
4645 OO
.ui
.mixin
.FloatableElement
.prototype.isFloatableOutOfView = function () {
4646 return this.floatableOutOfView
;
4650 * Position the floatable below its container.
4652 * This should only be done when both of them are attached to the DOM and visible.
4655 * @return {OO.ui.Element} The element, for chaining
4657 OO
.ui
.mixin
.FloatableElement
.prototype.position = function () {
4658 if ( !this.positioning
) {
4663 // To continue, some things need to be true:
4664 // The element must actually be in the DOM
4665 this.isElementAttached() && (
4666 // The closest scrollable is the current window
4667 this.$floatableClosestScrollable
[ 0 ] === this.getElementWindow() ||
4668 // OR is an element in the element's DOM
4669 $.contains( this.getElementDocument(), this.$floatableClosestScrollable
[ 0 ] )
4672 // Abort early if important parts of the widget are no longer attached to the DOM
4676 this.floatableOutOfView
= this.hideWhenOutOfView
&& !this.isElementInViewport( this.$floatableContainer
, this.$floatableClosestScrollable
);
4677 if ( this.floatableOutOfView
) {
4678 this.$floatable
.addClass( 'oo-ui-element-hidden' );
4681 this.$floatable
.removeClass( 'oo-ui-element-hidden' );
4684 this.$floatable
.css( this.computePosition() );
4686 // We updated the position, so re-evaluate the clipping state.
4687 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
4688 // will not notice the need to update itself.)
4689 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
4690 // it not listen to the right events in the right places?
4699 * Compute how #$floatable should be positioned based on the position of #$floatableContainer
4700 * and the positioning settings. This is a helper for #position that shouldn't be called directly,
4701 * but may be overridden by subclasses if they want to change or add to the positioning logic.
4703 * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
4705 OO
.ui
.mixin
.FloatableElement
.prototype.computePosition = function () {
4706 var isBody
, scrollableX
, scrollableY
, containerPos
,
4707 horizScrollbarHeight
, vertScrollbarWidth
, scrollTop
, scrollLeft
,
4708 newPos
= { top
: '', left
: '', bottom
: '', right
: '' },
4709 direction
= this.$floatableContainer
.css( 'direction' ),
4710 $offsetParent
= this.$floatable
.offsetParent();
4712 if ( $offsetParent
.is( 'html' ) ) {
4713 // The innerHeight/Width and clientHeight/Width calculations don't work well on the
4714 // <html> element, but they do work on the <body>
4715 $offsetParent
= $( $offsetParent
[ 0 ].ownerDocument
.body
);
4717 isBody
= $offsetParent
.is( 'body' );
4718 scrollableX
= $offsetParent
.css( 'overflow-x' ) === 'scroll' || $offsetParent
.css( 'overflow-x' ) === 'auto';
4719 scrollableY
= $offsetParent
.css( 'overflow-y' ) === 'scroll' || $offsetParent
.css( 'overflow-y' ) === 'auto';
4721 vertScrollbarWidth
= $offsetParent
.innerWidth() - $offsetParent
.prop( 'clientWidth' );
4722 horizScrollbarHeight
= $offsetParent
.innerHeight() - $offsetParent
.prop( 'clientHeight' );
4723 // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
4724 // or if it isn't scrollable
4725 scrollTop
= scrollableY
&& !isBody
? $offsetParent
.scrollTop() : 0;
4726 scrollLeft
= scrollableX
&& !isBody
? OO
.ui
.Element
.static.getScrollLeft( $offsetParent
[ 0 ] ) : 0;
4728 // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
4729 // if the <body> has a margin
4730 containerPos
= isBody
?
4731 this.$floatableContainer
.offset() :
4732 OO
.ui
.Element
.static.getRelativePosition( this.$floatableContainer
, $offsetParent
);
4733 containerPos
.bottom
= containerPos
.top
+ this.$floatableContainer
.outerHeight();
4734 containerPos
.right
= containerPos
.left
+ this.$floatableContainer
.outerWidth();
4735 containerPos
.start
= direction
=== 'rtl' ? containerPos
.right
: containerPos
.left
;
4736 containerPos
.end
= direction
=== 'rtl' ? containerPos
.left
: containerPos
.right
;
4738 if ( this.verticalPosition
=== 'below' ) {
4739 newPos
.top
= containerPos
.bottom
;
4740 } else if ( this.verticalPosition
=== 'above' ) {
4741 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.top
;
4742 } else if ( this.verticalPosition
=== 'top' ) {
4743 newPos
.top
= containerPos
.top
;
4744 } else if ( this.verticalPosition
=== 'bottom' ) {
4745 newPos
.bottom
= $offsetParent
.outerHeight() - containerPos
.bottom
;
4746 } else if ( this.verticalPosition
=== 'center' ) {
4747 newPos
.top
= containerPos
.top
+
4748 ( this.$floatableContainer
.height() - this.$floatable
.height() ) / 2;
4751 if ( this.horizontalPosition
=== 'before' ) {
4752 newPos
.end
= containerPos
.start
;
4753 } else if ( this.horizontalPosition
=== 'after' ) {
4754 newPos
.start
= containerPos
.end
;
4755 } else if ( this.horizontalPosition
=== 'start' ) {
4756 newPos
.start
= containerPos
.start
;
4757 } else if ( this.horizontalPosition
=== 'end' ) {
4758 newPos
.end
= containerPos
.end
;
4759 } else if ( this.horizontalPosition
=== 'center' ) {
4760 newPos
.left
= containerPos
.left
+
4761 ( this.$floatableContainer
.width() - this.$floatable
.width() ) / 2;
4764 if ( newPos
.start
!== undefined ) {
4765 if ( direction
=== 'rtl' ) {
4766 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.start
;
4768 newPos
.left
= newPos
.start
;
4770 delete newPos
.start
;
4772 if ( newPos
.end
!== undefined ) {
4773 if ( direction
=== 'rtl' ) {
4774 newPos
.left
= newPos
.end
;
4776 newPos
.right
= ( isBody
? $( $offsetParent
[ 0 ].ownerDocument
.documentElement
) : $offsetParent
).outerWidth() - newPos
.end
;
4781 // Account for scroll position
4782 if ( newPos
.top
!== '' ) {
4783 newPos
.top
+= scrollTop
;
4785 if ( newPos
.bottom
!== '' ) {
4786 newPos
.bottom
-= scrollTop
;
4788 if ( newPos
.left
!== '' ) {
4789 newPos
.left
+= scrollLeft
;
4791 if ( newPos
.right
!== '' ) {
4792 newPos
.right
-= scrollLeft
;
4795 // Account for scrollbar gutter
4796 if ( newPos
.bottom
!== '' ) {
4797 newPos
.bottom
-= horizScrollbarHeight
;
4799 if ( direction
=== 'rtl' ) {
4800 if ( newPos
.left
!== '' ) {
4801 newPos
.left
-= vertScrollbarWidth
;
4804 if ( newPos
.right
!== '' ) {
4805 newPos
.right
-= vertScrollbarWidth
;
4813 * Element that can be automatically clipped to visible boundaries.
4815 * Whenever the element's natural height changes, you have to call
4816 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
4817 * clipping correctly.
4819 * The dimensions of #$clippableContainer will be compared to the boundaries of the
4820 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
4821 * then #$clippable will be given a fixed reduced height and/or width and will be made
4822 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
4823 * but you can build a static footer by setting #$clippableContainer to an element that contains
4824 * #$clippable and the footer.
4830 * @param {Object} [config] Configuration options
4831 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
4832 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
4833 * omit to use #$clippable
4835 OO
.ui
.mixin
.ClippableElement
= function OoUiMixinClippableElement( config
) {
4836 // Configuration initialization
4837 config
= config
|| {};
4840 this.$clippable
= null;
4841 this.$clippableContainer
= null;
4842 this.clipping
= false;
4843 this.clippedHorizontally
= false;
4844 this.clippedVertically
= false;
4845 this.$clippableScrollableContainer
= null;
4846 this.$clippableScroller
= null;
4847 this.$clippableWindow
= null;
4848 this.idealWidth
= null;
4849 this.idealHeight
= null;
4850 this.onClippableScrollHandler
= this.clip
.bind( this );
4851 this.onClippableWindowResizeHandler
= this.clip
.bind( this );
4854 if ( config
.$clippableContainer
) {
4855 this.setClippableContainer( config
.$clippableContainer
);
4857 this.setClippableElement( config
.$clippable
|| this.$element
);
4863 * Set clippable element.
4865 * If an element is already set, it will be cleaned up before setting up the new element.
4867 * @param {jQuery} $clippable Element to make clippable
4869 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableElement = function ( $clippable
) {
4870 if ( this.$clippable
) {
4871 this.$clippable
.removeClass( 'oo-ui-clippableElement-clippable' );
4872 this.$clippable
.css( { width
: '', height
: '', overflowX
: '', overflowY
: '' } );
4873 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4876 this.$clippable
= $clippable
.addClass( 'oo-ui-clippableElement-clippable' );
4881 * Set clippable container.
4883 * This is the container that will be measured when deciding whether to clip. When clipping,
4884 * #$clippable will be resized in order to keep the clippable container fully visible.
4886 * If the clippable container is unset, #$clippable will be used.
4888 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
4890 OO
.ui
.mixin
.ClippableElement
.prototype.setClippableContainer = function ( $clippableContainer
) {
4891 this.$clippableContainer
= $clippableContainer
;
4892 if ( this.$clippable
) {
4900 * Do not turn clipping on until after the element is attached to the DOM and visible.
4902 * @param {boolean} [clipping] Enable clipping, omit to toggle
4904 * @return {OO.ui.Element} The element, for chaining
4906 OO
.ui
.mixin
.ClippableElement
.prototype.toggleClipping = function ( clipping
) {
4907 clipping
= clipping
=== undefined ? !this.clipping
: !!clipping
;
4909 if ( clipping
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
4910 OO
.ui
.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
4911 this.warnedUnattached
= true;
4914 if ( this.clipping
!== clipping
) {
4915 this.clipping
= clipping
;
4917 this.$clippableScrollableContainer
= $( this.getClosestScrollableElementContainer() );
4918 // If the clippable container is the root, we have to listen to scroll events and check
4919 // jQuery.scrollTop on the window because of browser inconsistencies
4920 this.$clippableScroller
= this.$clippableScrollableContainer
.is( 'html, body' ) ?
4921 $( OO
.ui
.Element
.static.getWindow( this.$clippableScrollableContainer
) ) :
4922 this.$clippableScrollableContainer
;
4923 this.$clippableScroller
.on( 'scroll', this.onClippableScrollHandler
);
4924 this.$clippableWindow
= $( this.getElementWindow() )
4925 .on( 'resize', this.onClippableWindowResizeHandler
);
4926 // Initial clip after visible
4929 this.$clippable
.css( {
4937 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
4939 this.$clippableScrollableContainer
= null;
4940 this.$clippableScroller
.off( 'scroll', this.onClippableScrollHandler
);
4941 this.$clippableScroller
= null;
4942 this.$clippableWindow
.off( 'resize', this.onClippableWindowResizeHandler
);
4943 this.$clippableWindow
= null;
4951 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4953 * @return {boolean} Element will be clipped to the visible area
4955 OO
.ui
.mixin
.ClippableElement
.prototype.isClipping = function () {
4956 return this.clipping
;
4960 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4962 * @return {boolean} Part of the element is being clipped
4964 OO
.ui
.mixin
.ClippableElement
.prototype.isClipped = function () {
4965 return this.clippedHorizontally
|| this.clippedVertically
;
4969 * Check if the right of the element is being clipped by the nearest scrollable container.
4971 * @return {boolean} Part of the element is being clipped
4973 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedHorizontally = function () {
4974 return this.clippedHorizontally
;
4978 * Check if the bottom of the element is being clipped by the nearest scrollable container.
4980 * @return {boolean} Part of the element is being clipped
4982 OO
.ui
.mixin
.ClippableElement
.prototype.isClippedVertically = function () {
4983 return this.clippedVertically
;
4987 * Set the ideal size. These are the dimensions #$clippable will have when it's not being clipped.
4989 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4990 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4992 OO
.ui
.mixin
.ClippableElement
.prototype.setIdealSize = function ( width
, height
) {
4993 this.idealWidth
= width
;
4994 this.idealHeight
= height
;
4996 if ( !this.clipping
) {
4997 // Update dimensions
4998 this.$clippable
.css( { width
: width
, height
: height
} );
5000 // While clipping, idealWidth and idealHeight are not considered
5004 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5005 * ClippableElement will clip the opposite side when reducing element's width.
5007 * Classes that mix in ClippableElement should override this to return 'right' if their
5008 * clippable is absolutely positioned and using 'right: Npx' (and not using 'left').
5009 * If your class also mixes in FloatableElement, this is handled automatically.
5011 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5012 * always in pixels, even if they were unset or set to 'auto'.)
5014 * When in doubt, 'left' (or 'right' in RTL) is a sane fallback.
5016 * @return {string} 'left' or 'right'
5018 OO
.ui
.mixin
.ClippableElement
.prototype.getHorizontalAnchorEdge = function () {
5019 if ( this.computePosition
&& this.positioning
&& this.computePosition().right
!== '' ) {
5026 * Return the side of the clippable on which it is "anchored" (aligned to something else).
5027 * ClippableElement will clip the opposite side when reducing element's width.
5029 * Classes that mix in ClippableElement should override this to return 'bottom' if their
5030 * clippable is absolutely positioned and using 'bottom: Npx' (and not using 'top').
5031 * If your class also mixes in FloatableElement, this is handled automatically.
5033 * (This can't be guessed from the actual CSS because the computed values for 'left'/'right' are
5034 * always in pixels, even if they were unset or set to 'auto'.)
5036 * When in doubt, 'top' is a sane fallback.
5038 * @return {string} 'top' or 'bottom'
5040 OO
.ui
.mixin
.ClippableElement
.prototype.getVerticalAnchorEdge = function () {
5041 if ( this.computePosition
&& this.positioning
&& this.computePosition().bottom
!== '' ) {
5048 * Clip element to visible boundaries and allow scrolling when needed. You should call this method
5049 * when the element's natural height changes.
5051 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5052 * overlapped by, the visible area of the nearest scrollable container.
5054 * Because calling clip() when the natural height changes isn't always possible, we also set
5055 * max-height when the element isn't being clipped. This means that if the element tries to grow
5056 * beyond the edge, something reasonable will happen before clip() is called.
5059 * @return {OO.ui.Element} The element, for chaining
5061 OO
.ui
.mixin
.ClippableElement
.prototype.clip = function () {
5062 var extraHeight
, extraWidth
, viewportSpacing
,
5063 desiredWidth
, desiredHeight
, allotedWidth
, allotedHeight
,
5064 naturalWidth
, naturalHeight
, clipWidth
, clipHeight
,
5065 $item
, itemRect
, $viewport
, viewportRect
, availableRect
,
5066 direction
, vertScrollbarWidth
, horizScrollbarHeight
,
5067 // Extra tolerance so that the sloppy code below doesn't result in results that are off
5068 // by one or two pixels. (And also so that we have space to display drop shadows.)
5069 // Chosen by fair dice roll.
5072 if ( !this.clipping
) {
5073 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
5077 function rectIntersection( a
, b
) {
5079 out
.top
= Math
.max( a
.top
, b
.top
);
5080 out
.left
= Math
.max( a
.left
, b
.left
);
5081 out
.bottom
= Math
.min( a
.bottom
, b
.bottom
);
5082 out
.right
= Math
.min( a
.right
, b
.right
);
5086 viewportSpacing
= OO
.ui
.getViewportSpacing();
5088 if ( this.$clippableScrollableContainer
.is( 'html, body' ) ) {
5089 $viewport
= $( this.$clippableScrollableContainer
[ 0 ].ownerDocument
.body
);
5090 // Dimensions of the browser window, rather than the element!
5094 right
: document
.documentElement
.clientWidth
,
5095 bottom
: document
.documentElement
.clientHeight
5097 viewportRect
.top
+= viewportSpacing
.top
;
5098 viewportRect
.left
+= viewportSpacing
.left
;
5099 viewportRect
.right
-= viewportSpacing
.right
;
5100 viewportRect
.bottom
-= viewportSpacing
.bottom
;
5102 $viewport
= this.$clippableScrollableContainer
;
5103 viewportRect
= $viewport
[ 0 ].getBoundingClientRect();
5104 // Convert into a plain object
5105 viewportRect
= $.extend( {}, viewportRect
);
5108 // Account for scrollbar gutter
5109 direction
= $viewport
.css( 'direction' );
5110 vertScrollbarWidth
= $viewport
.innerWidth() - $viewport
.prop( 'clientWidth' );
5111 horizScrollbarHeight
= $viewport
.innerHeight() - $viewport
.prop( 'clientHeight' );
5112 viewportRect
.bottom
-= horizScrollbarHeight
;
5113 if ( direction
=== 'rtl' ) {
5114 viewportRect
.left
+= vertScrollbarWidth
;
5116 viewportRect
.right
-= vertScrollbarWidth
;
5119 // Add arbitrary tolerance
5120 viewportRect
.top
+= buffer
;
5121 viewportRect
.left
+= buffer
;
5122 viewportRect
.right
-= buffer
;
5123 viewportRect
.bottom
-= buffer
;
5125 $item
= this.$clippableContainer
|| this.$clippable
;
5127 extraHeight
= $item
.outerHeight() - this.$clippable
.outerHeight();
5128 extraWidth
= $item
.outerWidth() - this.$clippable
.outerWidth();
5130 itemRect
= $item
[ 0 ].getBoundingClientRect();
5131 // Convert into a plain object
5132 itemRect
= $.extend( {}, itemRect
);
5134 // Item might already be clipped, so we can't just use its dimensions (in case we might need to
5135 // make it larger than before). Extend the rectangle to the maximum size we are allowed to take.
5136 if ( this.getHorizontalAnchorEdge() === 'right' ) {
5137 itemRect
.left
= viewportRect
.left
;
5139 itemRect
.right
= viewportRect
.right
;
5141 if ( this.getVerticalAnchorEdge() === 'bottom' ) {
5142 itemRect
.top
= viewportRect
.top
;
5144 itemRect
.bottom
= viewportRect
.bottom
;
5147 availableRect
= rectIntersection( viewportRect
, itemRect
);
5149 desiredWidth
= Math
.max( 0, availableRect
.right
- availableRect
.left
);
5150 desiredHeight
= Math
.max( 0, availableRect
.bottom
- availableRect
.top
);
5151 // It should never be desirable to exceed the dimensions of the browser viewport... right?
5152 desiredWidth
= Math
.min( desiredWidth
,
5153 document
.documentElement
.clientWidth
- viewportSpacing
.left
- viewportSpacing
.right
);
5154 desiredHeight
= Math
.min( desiredHeight
,
5155 document
.documentElement
.clientHeight
- viewportSpacing
.top
- viewportSpacing
.right
);
5156 allotedWidth
= Math
.ceil( desiredWidth
- extraWidth
);
5157 allotedHeight
= Math
.ceil( desiredHeight
- extraHeight
);
5158 naturalWidth
= this.$clippable
.prop( 'scrollWidth' );
5159 naturalHeight
= this.$clippable
.prop( 'scrollHeight' );
5160 clipWidth
= allotedWidth
< naturalWidth
;
5161 clipHeight
= allotedHeight
< naturalHeight
;
5164 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5165 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5166 this.$clippable
.css( 'overflowX', 'scroll' );
5167 // eslint-disable-next-line no-void
5168 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5169 this.$clippable
.css( {
5170 width
: Math
.max( 0, allotedWidth
),
5174 this.$clippable
.css( {
5176 width
: this.idealWidth
|| '',
5177 maxWidth
: Math
.max( 0, allotedWidth
)
5181 // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
5182 // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
5183 this.$clippable
.css( 'overflowY', 'scroll' );
5184 // eslint-disable-next-line no-void
5185 void this.$clippable
[ 0 ].offsetHeight
; // Force reflow
5186 this.$clippable
.css( {
5187 height
: Math
.max( 0, allotedHeight
),
5191 this.$clippable
.css( {
5193 height
: this.idealHeight
|| '',
5194 maxHeight
: Math
.max( 0, allotedHeight
)
5198 // If we stopped clipping in at least one of the dimensions
5199 if ( ( this.clippedHorizontally
&& !clipWidth
) || ( this.clippedVertically
&& !clipHeight
) ) {
5200 OO
.ui
.Element
.static.reconsiderScrollbars( this.$clippable
[ 0 ] );
5203 this.clippedHorizontally
= clipWidth
;
5204 this.clippedVertically
= clipHeight
;
5210 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
5211 * By default, each popup has an anchor that points toward its origin.
5212 * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
5214 * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
5218 * var popup = new OO.ui.PopupWidget( {
5219 * $content: $( '<p>Hi there!</p>' ),
5224 * $( document.body ).append( popup.$element );
5225 * // To display the popup, toggle the visibility to 'true'.
5226 * popup.toggle( true );
5228 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups
5231 * @extends OO.ui.Widget
5232 * @mixins OO.ui.mixin.LabelElement
5233 * @mixins OO.ui.mixin.ClippableElement
5234 * @mixins OO.ui.mixin.FloatableElement
5237 * @param {Object} [config] Configuration options
5238 * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
5239 * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
5240 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
5241 * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
5242 * 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
5243 * of $floatableContainer
5244 * 'below': Put popup below $floatableContainer; anchor points up to the horizontal center
5245 * of $floatableContainer
5246 * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
5247 * endwards (right/left) to the vertical center of $floatableContainer
5248 * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
5249 * startwards (left/right) to the vertical center of $floatableContainer
5250 * @cfg {string} [align='center'] How to align the popup to $floatableContainer
5251 * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
5252 * as possible while still keeping the anchor within the popup;
5253 * if position is before/after, move the popup as far downwards as possible.
5254 * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
5255 * as possible while still keeping the anchor within the popup;
5256 * if position in before/after, move the popup as far upwards as possible.
5257 * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
5258 * of the popup with the center of $floatableContainer.
5259 * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
5260 * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
5261 * @cfg {boolean} [autoFlip=true] Whether to automatically switch the popup's position between
5262 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5263 * desired direction to display the popup without clipping
5264 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
5265 * See the [OOUI docs on MediaWiki][3] for an example.
5266 * [3]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#containerExample
5267 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
5268 * @cfg {jQuery} [$content] Content to append to the popup's body
5269 * @cfg {jQuery} [$footer] Content to append to the popup's footer
5270 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
5271 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
5272 * This config option is only relevant if #autoClose is set to `true`. See the [OOUI documentation on MediaWiki][2]
5274 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Popups#autocloseExample
5275 * @cfg {boolean} [head=false] Show a popup header that contains a #label (if specified) and close
5277 * @cfg {boolean} [padded=false] Add padding to the popup's body
5279 OO
.ui
.PopupWidget
= function OoUiPopupWidget( config
) {
5280 // Configuration initialization
5281 config
= config
|| {};
5283 // Parent constructor
5284 OO
.ui
.PopupWidget
.parent
.call( this, config
);
5286 // Properties (must be set before ClippableElement constructor call)
5287 this.$body
= $( '<div>' );
5288 this.$popup
= $( '<div>' );
5290 // Mixin constructors
5291 OO
.ui
.mixin
.LabelElement
.call( this, config
);
5292 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, {
5293 $clippable
: this.$body
,
5294 $clippableContainer
: this.$popup
5296 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
5299 this.$anchor
= $( '<div>' );
5300 // If undefined, will be computed lazily in computePosition()
5301 this.$container
= config
.$container
;
5302 this.containerPadding
= config
.containerPadding
!== undefined ? config
.containerPadding
: 10;
5303 this.autoClose
= !!config
.autoClose
;
5304 this.transitionTimeout
= null;
5305 this.anchored
= false;
5306 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
5307 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
5310 this.setSize( config
.width
, config
.height
);
5311 this.toggleAnchor( config
.anchor
=== undefined || config
.anchor
);
5312 this.setAlignment( config
.align
|| 'center' );
5313 this.setPosition( config
.position
|| 'below' );
5314 this.setAutoFlip( config
.autoFlip
=== undefined || config
.autoFlip
);
5315 this.setAutoCloseIgnore( config
.$autoCloseIgnore
);
5316 this.$body
.addClass( 'oo-ui-popupWidget-body' );
5317 this.$anchor
.addClass( 'oo-ui-popupWidget-anchor' );
5319 .addClass( 'oo-ui-popupWidget-popup' )
5320 .append( this.$body
);
5322 .addClass( 'oo-ui-popupWidget' )
5323 .append( this.$popup
, this.$anchor
);
5324 // Move content, which was added to #$element by OO.ui.Widget, to the body
5325 // FIXME This is gross, we should use '$body' or something for the config
5326 if ( config
.$content
instanceof $ ) {
5327 this.$body
.append( config
.$content
);
5330 if ( config
.padded
) {
5331 this.$body
.addClass( 'oo-ui-popupWidget-body-padded' );
5334 if ( config
.head
) {
5335 this.closeButton
= new OO
.ui
.ButtonWidget( { framed
: false, icon
: 'close' } );
5336 this.closeButton
.connect( this, { click
: 'onCloseButtonClick' } );
5337 this.$head
= $( '<div>' )
5338 .addClass( 'oo-ui-popupWidget-head' )
5339 .append( this.$label
, this.closeButton
.$element
);
5340 this.$popup
.prepend( this.$head
);
5343 if ( config
.$footer
) {
5344 this.$footer
= $( '<div>' )
5345 .addClass( 'oo-ui-popupWidget-footer' )
5346 .append( config
.$footer
);
5347 this.$popup
.append( this.$footer
);
5350 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5351 // that reference properties not initialized at that time of parent class construction
5352 // TODO: Find a better way to handle post-constructor setup
5353 this.visible
= false;
5354 this.$element
.addClass( 'oo-ui-element-hidden' );
5359 OO
.inheritClass( OO
.ui
.PopupWidget
, OO
.ui
.Widget
);
5360 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.LabelElement
);
5361 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.ClippableElement
);
5362 OO
.mixinClass( OO
.ui
.PopupWidget
, OO
.ui
.mixin
.FloatableElement
);
5369 * The popup is ready: it is visible and has been positioned and clipped.
5375 * Handles document mouse down events.
5378 * @param {MouseEvent} e Mouse down event
5380 OO
.ui
.PopupWidget
.prototype.onDocumentMouseDown = function ( e
) {
5383 !OO
.ui
.contains( this.$element
.add( this.$autoCloseIgnore
).get(), e
.target
, true )
5385 this.toggle( false );
5389 // Deprecated alias since 0.28.3
5390 OO
.ui
.PopupWidget
.prototype.onMouseDown = function () {
5391 OO
.ui
.warnDeprecation( 'onMouseDown is deprecated, use onDocumentMouseDown instead' );
5392 this.onDocumentMouseDown
.apply( this, arguments
);
5396 * Bind document mouse down listener.
5400 OO
.ui
.PopupWidget
.prototype.bindDocumentMouseDownListener = function () {
5401 // Capture clicks outside popup
5402 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5403 // We add 'click' event because iOS safari needs to respond to this event.
5404 // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
5405 // then it will trigger when scrolling. While iOS Safari has some reported behavior
5406 // of occasionally not emitting 'click' properly, that event seems to be the standard
5407 // that it should be emitting, so we add it to this and will operate the event handler
5408 // on whichever of these events was triggered first
5409 this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5412 // Deprecated alias since 0.28.3
5413 OO
.ui
.PopupWidget
.prototype.bindMouseDownListener = function () {
5414 OO
.ui
.warnDeprecation( 'bindMouseDownListener is deprecated, use bindDocumentMouseDownListener instead' );
5415 this.bindDocumentMouseDownListener
.apply( this, arguments
);
5419 * Handles close button click events.
5423 OO
.ui
.PopupWidget
.prototype.onCloseButtonClick = function () {
5424 if ( this.isVisible() ) {
5425 this.toggle( false );
5430 * Unbind document mouse down listener.
5434 OO
.ui
.PopupWidget
.prototype.unbindDocumentMouseDownListener = function () {
5435 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
5436 this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler
, true );
5439 // Deprecated alias since 0.28.3
5440 OO
.ui
.PopupWidget
.prototype.unbindMouseDownListener = function () {
5441 OO
.ui
.warnDeprecation( 'unbindMouseDownListener is deprecated, use unbindDocumentMouseDownListener instead' );
5442 this.unbindDocumentMouseDownListener
.apply( this, arguments
);
5446 * Handles document key down events.
5449 * @param {KeyboardEvent} e Key down event
5451 OO
.ui
.PopupWidget
.prototype.onDocumentKeyDown = function ( e
) {
5453 e
.which
=== OO
.ui
.Keys
.ESCAPE
&&
5456 this.toggle( false );
5458 e
.stopPropagation();
5463 * Bind document key down listener.
5467 OO
.ui
.PopupWidget
.prototype.bindDocumentKeyDownListener = function () {
5468 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5471 // Deprecated alias since 0.28.3
5472 OO
.ui
.PopupWidget
.prototype.bindKeyDownListener = function () {
5473 OO
.ui
.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
5474 this.bindDocumentKeyDownListener
.apply( this, arguments
);
5478 * Unbind document key down listener.
5482 OO
.ui
.PopupWidget
.prototype.unbindDocumentKeyDownListener = function () {
5483 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
5486 // Deprecated alias since 0.28.3
5487 OO
.ui
.PopupWidget
.prototype.unbindKeyDownListener = function () {
5488 OO
.ui
.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
5489 this.unbindDocumentKeyDownListener
.apply( this, arguments
);
5493 * Show, hide, or toggle the visibility of the anchor.
5495 * @param {boolean} [show] Show anchor, omit to toggle
5497 OO
.ui
.PopupWidget
.prototype.toggleAnchor = function ( show
) {
5498 show
= show
=== undefined ? !this.anchored
: !!show
;
5500 if ( this.anchored
!== show
) {
5502 this.$element
.addClass( 'oo-ui-popupWidget-anchored' );
5503 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5505 this.$element
.removeClass( 'oo-ui-popupWidget-anchored' );
5506 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5508 this.anchored
= show
;
5513 * Change which edge the anchor appears on.
5515 * @param {string} edge 'top', 'bottom', 'start' or 'end'
5517 OO
.ui
.PopupWidget
.prototype.setAnchorEdge = function ( edge
) {
5518 if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge
) === -1 ) {
5519 throw new Error( 'Invalid value for edge: ' + edge
);
5521 if ( this.anchorEdge
!== null ) {
5522 this.$element
.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge
);
5524 this.anchorEdge
= edge
;
5525 if ( this.anchored
) {
5526 this.$element
.addClass( 'oo-ui-popupWidget-anchored-' + edge
);
5531 * Check if the anchor is visible.
5533 * @return {boolean} Anchor is visible
5535 OO
.ui
.PopupWidget
.prototype.hasAnchor = function () {
5536 return this.anchored
;
5540 * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
5541 * `.toggle( true )` after its #$element is attached to the DOM.
5543 * Do not show the popup while it is not attached to the DOM. The calculations required to display
5544 * it in the right place and with the right dimensions only work correctly while it is attached.
5545 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
5546 * strictly enforced, so currently it only generates a warning in the browser console.
5551 OO
.ui
.PopupWidget
.prototype.toggle = function ( show
) {
5552 var change
, normalHeight
, oppositeHeight
, normalWidth
, oppositeWidth
;
5553 show
= show
=== undefined ? !this.isVisible() : !!show
;
5555 change
= show
!== this.isVisible();
5557 if ( show
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
5558 OO
.ui
.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
5559 this.warnedUnattached
= true;
5561 if ( show
&& !this.$floatableContainer
&& this.isElementAttached() ) {
5562 // Fall back to the parent node if the floatableContainer is not set
5563 this.setFloatableContainer( this.$element
.parent() );
5566 if ( change
&& show
&& this.autoFlip
) {
5567 // Reset auto-flipping before showing the popup again. It's possible we no longer need to flip
5568 // (e.g. if the user scrolled).
5569 this.isAutoFlipped
= false;
5573 OO
.ui
.PopupWidget
.parent
.prototype.toggle
.call( this, show
);
5576 this.togglePositioning( show
&& !!this.$floatableContainer
);
5579 if ( this.autoClose
) {
5580 this.bindDocumentMouseDownListener();
5581 this.bindDocumentKeyDownListener();
5583 this.updateDimensions();
5584 this.toggleClipping( true );
5586 if ( this.autoFlip
) {
5587 if ( this.popupPosition
=== 'above' || this.popupPosition
=== 'below' ) {
5588 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5589 // If opening the popup in the normal direction causes it to be clipped, open
5590 // in the opposite one instead
5591 normalHeight
= this.$element
.height();
5592 this.isAutoFlipped
= !this.isAutoFlipped
;
5594 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
5595 // If that also causes it to be clipped, open in whichever direction
5596 // we have more space
5597 oppositeHeight
= this.$element
.height();
5598 if ( oppositeHeight
< normalHeight
) {
5599 this.isAutoFlipped
= !this.isAutoFlipped
;
5605 if ( this.popupPosition
=== 'before' || this.popupPosition
=== 'after' ) {
5606 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5607 // If opening the popup in the normal direction causes it to be clipped, open
5608 // in the opposite one instead
5609 normalWidth
= this.$element
.width();
5610 this.isAutoFlipped
= !this.isAutoFlipped
;
5611 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5612 // which causes positioning to be off. Toggle clipping back and fort to work around.
5613 this.toggleClipping( false );
5615 this.toggleClipping( true );
5616 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
5617 // If that also causes it to be clipped, open in whichever direction
5618 // we have more space
5619 oppositeWidth
= this.$element
.width();
5620 if ( oppositeWidth
< normalWidth
) {
5621 this.isAutoFlipped
= !this.isAutoFlipped
;
5622 // Due to T180173 horizontally clipped PopupWidgets have messed up dimensions,
5623 // which causes positioning to be off. Toggle clipping back and fort to work around.
5624 this.toggleClipping( false );
5626 this.toggleClipping( true );
5633 this.emit( 'ready' );
5635 this.toggleClipping( false );
5636 if ( this.autoClose
) {
5637 this.unbindDocumentMouseDownListener();
5638 this.unbindDocumentKeyDownListener();
5647 * Set the size of the popup.
5649 * Changing the size may also change the popup's position depending on the alignment.
5651 * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
5652 * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
5653 * @param {boolean} [transition=false] Use a smooth transition
5656 OO
.ui
.PopupWidget
.prototype.setSize = function ( width
, height
, transition
) {
5657 this.width
= width
!== undefined ? width
: 320;
5658 this.height
= height
!== undefined ? height
: null;
5659 if ( this.isVisible() ) {
5660 this.updateDimensions( transition
);
5665 * Update the size and position.
5667 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
5668 * be called automatically.
5670 * @param {boolean} [transition=false] Use a smooth transition
5673 OO
.ui
.PopupWidget
.prototype.updateDimensions = function ( transition
) {
5676 // Prevent transition from being interrupted
5677 clearTimeout( this.transitionTimeout
);
5679 // Enable transition
5680 this.$element
.addClass( 'oo-ui-popupWidget-transitioning' );
5686 // Prevent transitioning after transition is complete
5687 this.transitionTimeout
= setTimeout( function () {
5688 widget
.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5691 // Prevent transitioning immediately
5692 this.$element
.removeClass( 'oo-ui-popupWidget-transitioning' );
5699 OO
.ui
.PopupWidget
.prototype.computePosition = function () {
5700 var direction
, align
, vertical
, start
, end
, near
, far
, sizeProp
, popupSize
, anchorSize
, anchorPos
,
5701 anchorOffset
, anchorMargin
, parentPosition
, positionProp
, positionAdjustment
, floatablePos
,
5702 offsetParentPos
, containerPos
, popupPosition
, viewportSpacing
,
5704 anchorCss
= { left
: '', right
: '', top
: '', bottom
: '' },
5705 popupPositionOppositeMap
= {
5713 'force-left': 'backwards',
5714 'force-right': 'forwards'
5717 'force-left': 'forwards',
5718 'force-right': 'backwards'
5730 backwards
: this.anchored
? 'before' : 'end'
5738 if ( !this.$container
) {
5739 // Lazy-initialize $container if not specified in constructor
5740 this.$container
= $( this.getClosestScrollableElementContainer() );
5742 direction
= this.$container
.css( 'direction' );
5744 // Set height and width before we do anything else, since it might cause our measurements
5745 // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
5747 width
: this.width
!== null ? this.width
: 'auto',
5748 height
: this.height
!== null ? this.height
: 'auto'
5751 align
= alignMap
[ direction
][ this.align
] || this.align
;
5752 popupPosition
= this.popupPosition
;
5753 if ( this.isAutoFlipped
) {
5754 popupPosition
= popupPositionOppositeMap
[ popupPosition
];
5757 // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
5758 vertical
= popupPosition
=== 'before' || popupPosition
=== 'after';
5759 start
= vertical
? 'top' : ( direction
=== 'rtl' ? 'right' : 'left' );
5760 end
= vertical
? 'bottom' : ( direction
=== 'rtl' ? 'left' : 'right' );
5761 near
= vertical
? 'top' : 'left';
5762 far
= vertical
? 'bottom' : 'right';
5763 sizeProp
= vertical
? 'Height' : 'Width';
5764 popupSize
= vertical
? ( this.height
|| this.$popup
.height() ) : ( this.width
|| this.$popup
.width() );
5766 this.setAnchorEdge( anchorEdgeMap
[ popupPosition
] );
5767 this.horizontalPosition
= vertical
? popupPosition
: hPosMap
[ align
];
5768 this.verticalPosition
= vertical
? vPosMap
[ align
] : popupPosition
;
5771 parentPosition
= OO
.ui
.mixin
.FloatableElement
.prototype.computePosition
.call( this );
5772 // Find out which property FloatableElement used for positioning, and adjust that value
5773 positionProp
= vertical
?
5774 ( parentPosition
.top
!== '' ? 'top' : 'bottom' ) :
5775 ( parentPosition
.left
!== '' ? 'left' : 'right' );
5777 // Figure out where the near and far edges of the popup and $floatableContainer are
5778 floatablePos
= this.$floatableContainer
.offset();
5779 floatablePos
[ far
] = floatablePos
[ near
] + this.$floatableContainer
[ 'outer' + sizeProp
]();
5780 // Measure where the offsetParent is and compute our position based on that and parentPosition
5781 offsetParentPos
= this.$element
.offsetParent()[ 0 ] === document
.documentElement
?
5782 { top
: 0, left
: 0 } :
5783 this.$element
.offsetParent().offset();
5785 if ( positionProp
=== near
) {
5786 popupPos
[ near
] = offsetParentPos
[ near
] + parentPosition
[ near
];
5787 popupPos
[ far
] = popupPos
[ near
] + popupSize
;
5789 popupPos
[ far
] = offsetParentPos
[ near
] +
5790 this.$element
.offsetParent()[ 'inner' + sizeProp
]() - parentPosition
[ far
];
5791 popupPos
[ near
] = popupPos
[ far
] - popupSize
;
5794 if ( this.anchored
) {
5795 // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
5796 anchorPos
= ( floatablePos
[ start
] + floatablePos
[ end
] ) / 2;
5797 anchorOffset
= ( start
=== far
? -1 : 1 ) * ( anchorPos
- popupPos
[ start
] );
5799 // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
5800 // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
5801 anchorSize
= this.$anchor
[ 0 ][ 'scroll' + sizeProp
];
5802 anchorMargin
= parseFloat( this.$anchor
.css( 'margin-' + start
) );
5803 if ( anchorOffset
+ anchorMargin
< 2 * anchorSize
) {
5804 // Not enough space for the anchor on the start side; pull the popup startwards
5805 positionAdjustment
= ( positionProp
=== start
? -1 : 1 ) *
5806 ( 2 * anchorSize
- ( anchorOffset
+ anchorMargin
) );
5807 } else if ( anchorOffset
+ anchorMargin
> popupSize
- 2 * anchorSize
) {
5808 // Not enough space for the anchor on the end side; pull the popup endwards
5809 positionAdjustment
= ( positionProp
=== end
? -1 : 1 ) *
5810 ( anchorOffset
+ anchorMargin
- ( popupSize
- 2 * anchorSize
) );
5812 positionAdjustment
= 0;
5815 positionAdjustment
= 0;
5818 // Check if the popup will go beyond the edge of this.$container
5819 containerPos
= this.$container
[ 0 ] === document
.documentElement
?
5820 { top
: 0, left
: 0 } :
5821 this.$container
.offset();
5822 containerPos
[ far
] = containerPos
[ near
] + this.$container
[ 'inner' + sizeProp
]();
5823 if ( this.$container
[ 0 ] === document
.documentElement
) {
5824 viewportSpacing
= OO
.ui
.getViewportSpacing();
5825 containerPos
[ near
] += viewportSpacing
[ near
];
5826 containerPos
[ far
] -= viewportSpacing
[ far
];
5828 // Take into account how much the popup will move because of the adjustments we're going to make
5829 popupPos
[ near
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5830 popupPos
[ far
] += ( positionProp
=== near
? 1 : -1 ) * positionAdjustment
;
5831 if ( containerPos
[ near
] + this.containerPadding
> popupPos
[ near
] ) {
5832 // Popup goes beyond the near (left/top) edge, move it to the right/bottom
5833 positionAdjustment
+= ( positionProp
=== near
? 1 : -1 ) *
5834 ( containerPos
[ near
] + this.containerPadding
- popupPos
[ near
] );
5835 } else if ( containerPos
[ far
] - this.containerPadding
< popupPos
[ far
] ) {
5836 // Popup goes beyond the far (right/bottom) edge, move it to the left/top
5837 positionAdjustment
+= ( positionProp
=== far
? 1 : -1 ) *
5838 ( popupPos
[ far
] - ( containerPos
[ far
] - this.containerPadding
) );
5841 if ( this.anchored
) {
5842 // Adjust anchorOffset for positionAdjustment
5843 anchorOffset
+= ( positionProp
=== start
? -1 : 1 ) * positionAdjustment
;
5845 // Position the anchor
5846 anchorCss
[ start
] = anchorOffset
;
5847 this.$anchor
.css( anchorCss
);
5850 // Move the popup if needed
5851 parentPosition
[ positionProp
] += positionAdjustment
;
5853 return parentPosition
;
5857 * Set popup alignment
5859 * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
5860 * `backwards` or `forwards`.
5862 OO
.ui
.PopupWidget
.prototype.setAlignment = function ( align
) {
5863 // Validate alignment
5864 if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align
) > -1 ) {
5867 this.align
= 'center';
5873 * Get popup alignment
5875 * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
5876 * `backwards` or `forwards`.
5878 OO
.ui
.PopupWidget
.prototype.getAlignment = function () {
5883 * Change the positioning of the popup.
5885 * @param {string} position 'above', 'below', 'before' or 'after'
5887 OO
.ui
.PopupWidget
.prototype.setPosition = function ( position
) {
5888 if ( [ 'above', 'below', 'before', 'after' ].indexOf( position
) === -1 ) {
5891 this.popupPosition
= position
;
5896 * Get popup positioning.
5898 * @return {string} 'above', 'below', 'before' or 'after'
5900 OO
.ui
.PopupWidget
.prototype.getPosition = function () {
5901 return this.popupPosition
;
5905 * Set popup auto-flipping.
5907 * @param {boolean} autoFlip Whether to automatically switch the popup's position between
5908 * 'above' and 'below', or between 'before' and 'after', if there is not enough space in the
5909 * desired direction to display the popup without clipping
5911 OO
.ui
.PopupWidget
.prototype.setAutoFlip = function ( autoFlip
) {
5912 autoFlip
= !!autoFlip
;
5914 if ( this.autoFlip
!== autoFlip
) {
5915 this.autoFlip
= autoFlip
;
5920 * Set which elements will not close the popup when clicked.
5922 * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
5924 * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
5926 OO
.ui
.PopupWidget
.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore
) {
5927 this.$autoCloseIgnore
= $autoCloseIgnore
;
5931 * Get an ID of the body element, this can be used as the
5932 * `aria-describedby` attribute for an input field.
5934 * @return {string} The ID of the body element
5936 OO
.ui
.PopupWidget
.prototype.getBodyId = function () {
5937 var id
= this.$body
.attr( 'id' );
5938 if ( id
=== undefined ) {
5939 id
= OO
.ui
.generateElementId();
5940 this.$body
.attr( 'id', id
);
5946 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5947 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5948 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5949 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5955 * @param {Object} [config] Configuration options
5956 * @cfg {Object} [popup] Configuration to pass to popup
5957 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5959 OO
.ui
.mixin
.PopupElement
= function OoUiMixinPopupElement( config
) {
5960 // Configuration initialization
5961 config
= config
|| {};
5964 this.popup
= new OO
.ui
.PopupWidget( $.extend(
5967 $floatableContainer
: this.$element
5971 $autoCloseIgnore
: this.$element
.add( config
.popup
&& config
.popup
.$autoCloseIgnore
)
5981 * @return {OO.ui.PopupWidget} Popup widget
5983 OO
.ui
.mixin
.PopupElement
.prototype.getPopup = function () {
5988 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
5989 * which is used to display additional information or options.
5992 * // A PopupButtonWidget.
5993 * var popupButton = new OO.ui.PopupButtonWidget( {
5994 * label: 'Popup button with options',
5997 * $content: $( '<p>Additional options here.</p>' ),
5999 * align: 'force-left'
6002 * // Append the button to the DOM.
6003 * $( document.body ).append( popupButton.$element );
6006 * @extends OO.ui.ButtonWidget
6007 * @mixins OO.ui.mixin.PopupElement
6010 * @param {Object} [config] Configuration options
6011 * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
6012 * the expanded popup is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6013 * containing `<div>` and has a larger area. By default, the popup uses relative positioning.
6014 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
6016 OO
.ui
.PopupButtonWidget
= function OoUiPopupButtonWidget( config
) {
6017 // Configuration initialization
6018 config
= config
|| {};
6020 // Parent constructor
6021 OO
.ui
.PopupButtonWidget
.parent
.call( this, config
);
6023 // Mixin constructors
6024 OO
.ui
.mixin
.PopupElement
.call( this, config
);
6027 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
6030 this.connect( this, { click
: 'onAction' } );
6034 .addClass( 'oo-ui-popupButtonWidget' );
6036 .addClass( 'oo-ui-popupButtonWidget-popup' )
6037 .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
6038 .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
6039 this.$overlay
.append( this.popup
.$element
);
6044 OO
.inheritClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.ButtonWidget
);
6045 OO
.mixinClass( OO
.ui
.PopupButtonWidget
, OO
.ui
.mixin
.PopupElement
);
6050 * Handle the button action being triggered.
6054 OO
.ui
.PopupButtonWidget
.prototype.onAction = function () {
6055 this.popup
.toggle();
6059 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
6061 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
6066 * @mixins OO.ui.mixin.GroupElement
6069 * @param {Object} [config] Configuration options
6071 OO
.ui
.mixin
.GroupWidget
= function OoUiMixinGroupWidget( config
) {
6072 // Mixin constructors
6073 OO
.ui
.mixin
.GroupElement
.call( this, config
);
6078 OO
.mixinClass( OO
.ui
.mixin
.GroupWidget
, OO
.ui
.mixin
.GroupElement
);
6083 * Set the disabled state of the widget.
6085 * This will also update the disabled state of child widgets.
6087 * @param {boolean} disabled Disable widget
6089 * @return {OO.ui.Widget} The widget, for chaining
6091 OO
.ui
.mixin
.GroupWidget
.prototype.setDisabled = function ( disabled
) {
6095 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
6096 OO
.ui
.Widget
.prototype.setDisabled
.call( this, disabled
);
6098 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
6100 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6101 this.items
[ i
].updateDisabled();
6109 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
6111 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
6112 * allows bidirectional communication.
6114 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
6122 OO
.ui
.mixin
.ItemWidget
= function OoUiMixinItemWidget() {
6129 * Check if widget is disabled.
6131 * Checks parent if present, making disabled state inheritable.
6133 * @return {boolean} Widget is disabled
6135 OO
.ui
.mixin
.ItemWidget
.prototype.isDisabled = function () {
6136 return this.disabled
||
6137 ( this.elementGroup
instanceof OO
.ui
.Widget
&& this.elementGroup
.isDisabled() );
6141 * Set group element is in.
6143 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
6145 * @return {OO.ui.Widget} The widget, for chaining
6147 OO
.ui
.mixin
.ItemWidget
.prototype.setElementGroup = function ( group
) {
6149 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
6150 OO
.ui
.Element
.prototype.setElementGroup
.call( this, group
);
6152 // Initialize item disabled states
6153 this.updateDisabled();
6159 * OptionWidgets are special elements that can be selected and configured with data. The
6160 * data is often unique for each option, but it does not have to be. OptionWidgets are used
6161 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
6162 * and examples, please see the [OOUI documentation on MediaWiki][1].
6164 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6167 * @extends OO.ui.Widget
6168 * @mixins OO.ui.mixin.ItemWidget
6169 * @mixins OO.ui.mixin.LabelElement
6170 * @mixins OO.ui.mixin.FlaggedElement
6171 * @mixins OO.ui.mixin.AccessKeyedElement
6172 * @mixins OO.ui.mixin.TitledElement
6175 * @param {Object} [config] Configuration options
6177 OO
.ui
.OptionWidget
= function OoUiOptionWidget( config
) {
6178 // Configuration initialization
6179 config
= config
|| {};
6181 // Parent constructor
6182 OO
.ui
.OptionWidget
.parent
.call( this, config
);
6184 // Mixin constructors
6185 OO
.ui
.mixin
.ItemWidget
.call( this );
6186 OO
.ui
.mixin
.LabelElement
.call( this, config
);
6187 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
6188 OO
.ui
.mixin
.AccessKeyedElement
.call( this, config
);
6189 OO
.ui
.mixin
.TitledElement
.call( this, config
);
6192 this.selected
= false;
6193 this.highlighted
= false;
6194 this.pressed
= false;
6198 .data( 'oo-ui-optionWidget', this )
6199 // Allow programmatic focussing (and by accesskey), but not tabbing
6200 .attr( 'tabindex', '-1' )
6201 .attr( 'role', 'option' )
6202 .attr( 'aria-selected', 'false' )
6203 .addClass( 'oo-ui-optionWidget' )
6204 .append( this.$label
);
6209 OO
.inheritClass( OO
.ui
.OptionWidget
, OO
.ui
.Widget
);
6210 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.ItemWidget
);
6211 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.LabelElement
);
6212 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.FlaggedElement
);
6213 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
6214 OO
.mixinClass( OO
.ui
.OptionWidget
, OO
.ui
.mixin
.TitledElement
);
6216 /* Static Properties */
6219 * Whether this option can be selected. See #setSelected.
6223 * @property {boolean}
6225 OO
.ui
.OptionWidget
.static.selectable
= true;
6228 * Whether this option can be highlighted. See #setHighlighted.
6232 * @property {boolean}
6234 OO
.ui
.OptionWidget
.static.highlightable
= true;
6237 * Whether this option can be pressed. See #setPressed.
6241 * @property {boolean}
6243 OO
.ui
.OptionWidget
.static.pressable
= true;
6246 * Whether this option will be scrolled into view when it is selected.
6250 * @property {boolean}
6252 OO
.ui
.OptionWidget
.static.scrollIntoViewOnSelect
= false;
6257 * Check if the option can be selected.
6259 * @return {boolean} Item is selectable
6261 OO
.ui
.OptionWidget
.prototype.isSelectable = function () {
6262 return this.constructor.static.selectable
&& !this.disabled
&& this.isVisible();
6266 * Check if the option can be highlighted. A highlight indicates that the option
6267 * may be selected when a user presses enter or clicks. Disabled items cannot
6270 * @return {boolean} Item is highlightable
6272 OO
.ui
.OptionWidget
.prototype.isHighlightable = function () {
6273 return this.constructor.static.highlightable
&& !this.disabled
&& this.isVisible();
6277 * Check if the option can be pressed. The pressed state occurs when a user mouses
6278 * down on an item, but has not yet let go of the mouse.
6280 * @return {boolean} Item is pressable
6282 OO
.ui
.OptionWidget
.prototype.isPressable = function () {
6283 return this.constructor.static.pressable
&& !this.disabled
&& this.isVisible();
6287 * Check if the option is selected.
6289 * @return {boolean} Item is selected
6291 OO
.ui
.OptionWidget
.prototype.isSelected = function () {
6292 return this.selected
;
6296 * Check if the option is highlighted. A highlight indicates that the
6297 * item may be selected when a user presses enter or clicks.
6299 * @return {boolean} Item is highlighted
6301 OO
.ui
.OptionWidget
.prototype.isHighlighted = function () {
6302 return this.highlighted
;
6306 * Check if the option is pressed. The pressed state occurs when a user mouses
6307 * down on an item, but has not yet let go of the mouse. The item may appear
6308 * selected, but it will not be selected until the user releases the mouse.
6310 * @return {boolean} Item is pressed
6312 OO
.ui
.OptionWidget
.prototype.isPressed = function () {
6313 return this.pressed
;
6317 * Set the option’s selected state. In general, all modifications to the selection
6318 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
6319 * method instead of this method.
6321 * @param {boolean} [state=false] Select option
6323 * @return {OO.ui.Widget} The widget, for chaining
6325 OO
.ui
.OptionWidget
.prototype.setSelected = function ( state
) {
6326 if ( this.constructor.static.selectable
) {
6327 this.selected
= !!state
;
6329 .toggleClass( 'oo-ui-optionWidget-selected', state
)
6330 .attr( 'aria-selected', state
.toString() );
6331 if ( state
&& this.constructor.static.scrollIntoViewOnSelect
) {
6332 this.scrollElementIntoView();
6334 this.updateThemeClasses();
6340 * Set the option’s highlighted state. In general, all programmatic
6341 * modifications to the highlight should be handled by the
6342 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
6343 * method instead of this method.
6345 * @param {boolean} [state=false] Highlight option
6347 * @return {OO.ui.Widget} The widget, for chaining
6349 OO
.ui
.OptionWidget
.prototype.setHighlighted = function ( state
) {
6350 if ( this.constructor.static.highlightable
) {
6351 this.highlighted
= !!state
;
6352 this.$element
.toggleClass( 'oo-ui-optionWidget-highlighted', state
);
6353 this.updateThemeClasses();
6359 * Set the option’s pressed state. In general, all
6360 * programmatic modifications to the pressed state should be handled by the
6361 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
6362 * method instead of this method.
6364 * @param {boolean} [state=false] Press option
6366 * @return {OO.ui.Widget} The widget, for chaining
6368 OO
.ui
.OptionWidget
.prototype.setPressed = function ( state
) {
6369 if ( this.constructor.static.pressable
) {
6370 this.pressed
= !!state
;
6371 this.$element
.toggleClass( 'oo-ui-optionWidget-pressed', state
);
6372 this.updateThemeClasses();
6378 * Get text to match search strings against.
6380 * The default implementation returns the label text, but subclasses
6381 * can override this to provide more complex behavior.
6383 * @return {string|boolean} String to match search string against
6385 OO
.ui
.OptionWidget
.prototype.getMatchText = function () {
6386 var label
= this.getLabel();
6387 return typeof label
=== 'string' ? label
: this.$label
.text();
6391 * A SelectWidget is of a generic selection of options. The OOUI library contains several types of
6392 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
6393 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
6396 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
6397 * information, please see the [OOUI documentation on MediaWiki][1].
6400 * // A select widget with three options.
6401 * var select = new OO.ui.SelectWidget( {
6403 * new OO.ui.OptionWidget( {
6405 * label: 'Option One',
6407 * new OO.ui.OptionWidget( {
6409 * label: 'Option Two',
6411 * new OO.ui.OptionWidget( {
6413 * label: 'Option Three',
6417 * $( document.body ).append( select.$element );
6419 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6423 * @extends OO.ui.Widget
6424 * @mixins OO.ui.mixin.GroupWidget
6427 * @param {Object} [config] Configuration options
6428 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
6429 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
6430 * the [OOUI documentation on MediaWiki] [2] for examples.
6431 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
6433 OO
.ui
.SelectWidget
= function OoUiSelectWidget( config
) {
6434 // Configuration initialization
6435 config
= config
|| {};
6437 // Parent constructor
6438 OO
.ui
.SelectWidget
.parent
.call( this, config
);
6440 // Mixin constructors
6441 OO
.ui
.mixin
.GroupWidget
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
6444 this.pressed
= false;
6445 this.selecting
= null;
6446 this.onDocumentMouseUpHandler
= this.onDocumentMouseUp
.bind( this );
6447 this.onDocumentMouseMoveHandler
= this.onDocumentMouseMove
.bind( this );
6448 this.onDocumentKeyDownHandler
= this.onDocumentKeyDown
.bind( this );
6449 this.onDocumentKeyPressHandler
= this.onDocumentKeyPress
.bind( this );
6450 this.keyPressBuffer
= '';
6451 this.keyPressBufferTimer
= null;
6452 this.blockMouseOverEvents
= 0;
6455 this.connect( this, {
6459 focusin
: this.onFocus
.bind( this ),
6460 mousedown
: this.onMouseDown
.bind( this ),
6461 mouseover
: this.onMouseOver
.bind( this ),
6462 mouseleave
: this.onMouseLeave
.bind( this )
6467 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
6468 .attr( 'role', 'listbox' );
6469 this.setFocusOwner( this.$element
);
6470 if ( Array
.isArray( config
.items
) ) {
6471 this.addItems( config
.items
);
6477 OO
.inheritClass( OO
.ui
.SelectWidget
, OO
.ui
.Widget
);
6478 OO
.mixinClass( OO
.ui
.SelectWidget
, OO
.ui
.mixin
.GroupWidget
);
6485 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
6487 * @param {OO.ui.OptionWidget|null} item Highlighted item
6493 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
6494 * pressed state of an option.
6496 * @param {OO.ui.OptionWidget|null} item Pressed item
6502 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
6504 * @param {OO.ui.OptionWidget|null} item Selected item
6509 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
6510 * @param {OO.ui.OptionWidget} item Chosen item
6516 * An `add` event is emitted when options are added to the select with the #addItems method.
6518 * @param {OO.ui.OptionWidget[]} items Added items
6519 * @param {number} index Index of insertion point
6525 * A `remove` event is emitted when options are removed from the select with the #clearItems
6526 * or #removeItems methods.
6528 * @param {OO.ui.OptionWidget[]} items Removed items
6534 * Handle focus events
6537 * @param {jQuery.Event} event
6539 OO
.ui
.SelectWidget
.prototype.onFocus = function ( event
) {
6541 if ( event
.target
=== this.$element
[ 0 ] ) {
6542 // This widget was focussed, e.g. by the user tabbing to it.
6543 // The styles for focus state depend on one of the items being selected.
6544 if ( !this.findSelectedItem() ) {
6545 item
= this.findFirstSelectableItem();
6548 if ( event
.target
.tabIndex
=== -1 ) {
6549 // One of the options got focussed (and the event bubbled up here).
6550 // They can't be tabbed to, but they can be activated using accesskeys.
6551 // OptionWidgets and focusable UI elements inside them have tabindex="-1" set.
6552 item
= this.findTargetItem( event
);
6554 // There is something actually user-focusable in one of the labels of the options, and the
6555 // user focussed it (e.g. by tabbing to it). Do nothing (especially, don't change the focus).
6561 if ( item
.constructor.static.highlightable
) {
6562 this.highlightItem( item
);
6564 this.selectItem( item
);
6568 if ( event
.target
!== this.$element
[ 0 ] ) {
6569 this.$focusOwner
.focus();
6574 * Handle mouse down events.
6577 * @param {jQuery.Event} e Mouse down event
6578 * @return {undefined/boolean} False to prevent default if event is handled
6580 OO
.ui
.SelectWidget
.prototype.onMouseDown = function ( e
) {
6583 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
6584 this.togglePressed( true );
6585 item
= this.findTargetItem( e
);
6586 if ( item
&& item
.isSelectable() ) {
6587 this.pressItem( item
);
6588 this.selecting
= item
;
6589 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6590 this.getElementDocument().addEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6597 * Handle document mouse up events.
6600 * @param {MouseEvent} e Mouse up event
6601 * @return {undefined/boolean} False to prevent default if event is handled
6603 OO
.ui
.SelectWidget
.prototype.onDocumentMouseUp = function ( e
) {
6606 this.togglePressed( false );
6607 if ( !this.selecting
) {
6608 item
= this.findTargetItem( e
);
6609 if ( item
&& item
.isSelectable() ) {
6610 this.selecting
= item
;
6613 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
&& this.selecting
) {
6614 this.pressItem( null );
6615 this.chooseItem( this.selecting
);
6616 this.selecting
= null;
6619 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler
, true );
6620 this.getElementDocument().removeEventListener( 'mousemove', this.onDocumentMouseMoveHandler
, true );
6625 // Deprecated alias since 0.28.3
6626 OO
.ui
.SelectWidget
.prototype.onMouseUp = function () {
6627 OO
.ui
.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
6628 this.onDocumentMouseUp
.apply( this, arguments
);
6632 * Handle document mouse move events.
6635 * @param {MouseEvent} e Mouse move event
6637 OO
.ui
.SelectWidget
.prototype.onDocumentMouseMove = function ( e
) {
6640 if ( !this.isDisabled() && this.pressed
) {
6641 item
= this.findTargetItem( e
);
6642 if ( item
&& item
!== this.selecting
&& item
.isSelectable() ) {
6643 this.pressItem( item
);
6644 this.selecting
= item
;
6649 // Deprecated alias since 0.28.3
6650 OO
.ui
.SelectWidget
.prototype.onMouseMove = function () {
6651 OO
.ui
.warnDeprecation( 'onMouseMove is deprecated, use onDocumentMouseMove instead' );
6652 this.onDocumentMouseMove
.apply( this, arguments
);
6656 * Handle mouse over events.
6659 * @param {jQuery.Event} e Mouse over event
6660 * @return {undefined/boolean} False to prevent default if event is handled
6662 OO
.ui
.SelectWidget
.prototype.onMouseOver = function ( e
) {
6664 if ( this.blockMouseOverEvents
) {
6667 if ( !this.isDisabled() ) {
6668 item
= this.findTargetItem( e
);
6669 this.highlightItem( item
&& item
.isHighlightable() ? item
: null );
6675 * Handle mouse leave events.
6678 * @param {jQuery.Event} e Mouse over event
6679 * @return {undefined/boolean} False to prevent default if event is handled
6681 OO
.ui
.SelectWidget
.prototype.onMouseLeave = function () {
6682 if ( !this.isDisabled() ) {
6683 this.highlightItem( null );
6689 * Handle document key down events.
6692 * @param {KeyboardEvent} e Key down event
6694 OO
.ui
.SelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
6697 currentItem
= this.findHighlightedItem() || this.findSelectedItem();
6699 if ( !this.isDisabled() && this.isVisible() ) {
6700 switch ( e
.keyCode
) {
6701 case OO
.ui
.Keys
.ENTER
:
6702 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6703 // Was only highlighted, now let's select it. No-op if already selected.
6704 this.chooseItem( currentItem
);
6709 case OO
.ui
.Keys
.LEFT
:
6710 this.clearKeyPressBuffer();
6711 nextItem
= this.findRelativeSelectableItem( currentItem
, -1 );
6714 case OO
.ui
.Keys
.DOWN
:
6715 case OO
.ui
.Keys
.RIGHT
:
6716 this.clearKeyPressBuffer();
6717 nextItem
= this.findRelativeSelectableItem( currentItem
, 1 );
6720 case OO
.ui
.Keys
.ESCAPE
:
6721 case OO
.ui
.Keys
.TAB
:
6722 if ( currentItem
&& currentItem
.constructor.static.highlightable
) {
6723 currentItem
.setHighlighted( false );
6725 this.unbindDocumentKeyDownListener();
6726 this.unbindDocumentKeyPressListener();
6727 // Don't prevent tabbing away / defocusing
6733 if ( nextItem
.constructor.static.highlightable
) {
6734 this.highlightItem( nextItem
);
6736 this.chooseItem( nextItem
);
6738 this.scrollItemIntoView( nextItem
);
6743 e
.stopPropagation();
6748 // Deprecated alias since 0.28.3
6749 OO
.ui
.SelectWidget
.prototype.onKeyDown = function () {
6750 OO
.ui
.warnDeprecation( 'onKeyDown is deprecated, use onDocumentKeyDown instead' );
6751 this.onDocumentKeyDown
.apply( this, arguments
);
6755 * Bind document key down listener.
6759 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyDownListener = function () {
6760 this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6763 // Deprecated alias since 0.28.3
6764 OO
.ui
.SelectWidget
.prototype.bindKeyDownListener = function () {
6765 OO
.ui
.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
6766 this.bindDocumentKeyDownListener
.apply( this, arguments
);
6770 * Unbind document key down listener.
6774 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
6775 this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler
, true );
6778 // Deprecated alias since 0.28.3
6779 OO
.ui
.SelectWidget
.prototype.unbindKeyDownListener = function () {
6780 OO
.ui
.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
6781 this.unbindDocumentKeyDownListener
.apply( this, arguments
);
6785 * Scroll item into view, preventing spurious mouse highlight actions from happening.
6787 * @param {OO.ui.OptionWidget} item Item to scroll into view
6789 OO
.ui
.SelectWidget
.prototype.scrollItemIntoView = function ( item
) {
6791 // Chromium's Blink engine will generate spurious 'mouseover' events during programmatic scrolling
6792 // and around 100-150 ms after it is finished.
6793 this.blockMouseOverEvents
++;
6794 item
.scrollElementIntoView().done( function () {
6795 setTimeout( function () {
6796 widget
.blockMouseOverEvents
--;
6802 * Clear the key-press buffer
6806 OO
.ui
.SelectWidget
.prototype.clearKeyPressBuffer = function () {
6807 if ( this.keyPressBufferTimer
) {
6808 clearTimeout( this.keyPressBufferTimer
);
6809 this.keyPressBufferTimer
= null;
6811 this.keyPressBuffer
= '';
6815 * Handle key press events.
6818 * @param {KeyboardEvent} e Key press event
6819 * @return {undefined/boolean} False to prevent default if event is handled
6821 OO
.ui
.SelectWidget
.prototype.onDocumentKeyPress = function ( e
) {
6822 var c
, filter
, item
;
6824 if ( !e
.charCode
) {
6825 if ( e
.keyCode
=== OO
.ui
.Keys
.BACKSPACE
&& this.keyPressBuffer
!== '' ) {
6826 this.keyPressBuffer
= this.keyPressBuffer
.substr( 0, this.keyPressBuffer
.length
- 1 );
6831 // eslint-disable-next-line no-restricted-properties
6832 if ( String
.fromCodePoint
) {
6833 // eslint-disable-next-line no-restricted-properties
6834 c
= String
.fromCodePoint( e
.charCode
);
6836 c
= String
.fromCharCode( e
.charCode
);
6839 if ( this.keyPressBufferTimer
) {
6840 clearTimeout( this.keyPressBufferTimer
);
6842 this.keyPressBufferTimer
= setTimeout( this.clearKeyPressBuffer
.bind( this ), 1500 );
6844 item
= this.findHighlightedItem() || this.findSelectedItem();
6846 if ( this.keyPressBuffer
=== c
) {
6847 // Common (if weird) special case: typing "xxxx" will cycle through all
6848 // the items beginning with "x".
6850 item
= this.findRelativeSelectableItem( item
, 1 );
6853 this.keyPressBuffer
+= c
;
6856 filter
= this.getItemMatcher( this.keyPressBuffer
, false );
6857 if ( !item
|| !filter( item
) ) {
6858 item
= this.findRelativeSelectableItem( item
, 1, filter
);
6861 if ( this.isVisible() && item
.constructor.static.highlightable
) {
6862 this.highlightItem( item
);
6864 this.chooseItem( item
);
6866 this.scrollItemIntoView( item
);
6870 e
.stopPropagation();
6873 // Deprecated alias since 0.28.3
6874 OO
.ui
.SelectWidget
.prototype.onKeyPress = function () {
6875 OO
.ui
.warnDeprecation( 'onKeyPress is deprecated, use onDocumentKeyPress instead' );
6876 this.onDocumentKeyPress
.apply( this, arguments
);
6880 * Get a matcher for the specific string
6883 * @param {string} s String to match against items
6884 * @param {boolean} [exact=false] Only accept exact matches
6885 * @return {Function} function ( OO.ui.OptionWidget ) => boolean
6887 OO
.ui
.SelectWidget
.prototype.getItemMatcher = function ( s
, exact
) {
6890 // eslint-disable-next-line no-restricted-properties
6891 if ( s
.normalize
) {
6892 // eslint-disable-next-line no-restricted-properties
6895 s
= exact
? s
.trim() : s
.replace( /^\s+/, '' );
6896 re
= '^\\s*' + s
.replace( /([\\{}()|.?*+\-^$[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
6900 re
= new RegExp( re
, 'i' );
6901 return function ( item
) {
6902 var matchText
= item
.getMatchText();
6903 // eslint-disable-next-line no-restricted-properties
6904 if ( matchText
.normalize
) {
6905 // eslint-disable-next-line no-restricted-properties
6906 matchText
= matchText
.normalize();
6908 return re
.test( matchText
);
6913 * Bind document key press listener.
6917 OO
.ui
.SelectWidget
.prototype.bindDocumentKeyPressListener = function () {
6918 this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6921 // Deprecated alias since 0.28.3
6922 OO
.ui
.SelectWidget
.prototype.bindKeyPressListener = function () {
6923 OO
.ui
.warnDeprecation( 'bindKeyPressListener is deprecated, use bindDocumentKeyPressListener instead' );
6924 this.bindDocumentKeyPressListener
.apply( this, arguments
);
6928 * Unbind document key down listener.
6930 * If you override this, be sure to call this.clearKeyPressBuffer() from your
6935 OO
.ui
.SelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
6936 this.getElementDocument().removeEventListener( 'keypress', this.onDocumentKeyPressHandler
, true );
6937 this.clearKeyPressBuffer();
6940 // Deprecated alias since 0.28.3
6941 OO
.ui
.SelectWidget
.prototype.unbindKeyPressListener = function () {
6942 OO
.ui
.warnDeprecation( 'unbindKeyPressListener is deprecated, use unbindDocumentKeyPressListener instead' );
6943 this.unbindDocumentKeyPressListener
.apply( this, arguments
);
6947 * Visibility change handler
6950 * @param {boolean} visible
6952 OO
.ui
.SelectWidget
.prototype.onToggle = function ( visible
) {
6954 this.clearKeyPressBuffer();
6959 * Get the closest item to a jQuery.Event.
6962 * @param {jQuery.Event} e
6963 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
6965 OO
.ui
.SelectWidget
.prototype.findTargetItem = function ( e
) {
6966 var $option
= $( e
.target
).closest( '.oo-ui-optionWidget' );
6967 if ( !$option
.closest( '.oo-ui-selectWidget' ).is( this.$element
) ) {
6970 return $option
.data( 'oo-ui-optionWidget' ) || null;
6974 * Find selected item.
6976 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
6978 OO
.ui
.SelectWidget
.prototype.findSelectedItem = function () {
6981 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6982 if ( this.items
[ i
].isSelected() ) {
6983 return this.items
[ i
];
6990 * Find highlighted item.
6992 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
6994 OO
.ui
.SelectWidget
.prototype.findHighlightedItem = function () {
6997 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
6998 if ( this.items
[ i
].isHighlighted() ) {
6999 return this.items
[ i
];
7006 * Toggle pressed state.
7008 * Press is a state that occurs when a user mouses down on an item, but
7009 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
7010 * until the user releases the mouse.
7012 * @param {boolean} pressed An option is being pressed
7014 OO
.ui
.SelectWidget
.prototype.togglePressed = function ( pressed
) {
7015 if ( pressed
=== undefined ) {
7016 pressed
= !this.pressed
;
7018 if ( pressed
!== this.pressed
) {
7020 .toggleClass( 'oo-ui-selectWidget-pressed', pressed
)
7021 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed
);
7022 this.pressed
= pressed
;
7027 * Highlight an option. If the `item` param is omitted, no options will be highlighted
7028 * and any existing highlight will be removed. The highlight is mutually exclusive.
7030 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
7033 * @return {OO.ui.Widget} The widget, for chaining
7035 OO
.ui
.SelectWidget
.prototype.highlightItem = function ( item
) {
7036 var i
, len
, highlighted
,
7039 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7040 highlighted
= this.items
[ i
] === item
;
7041 if ( this.items
[ i
].isHighlighted() !== highlighted
) {
7042 this.items
[ i
].setHighlighted( highlighted
);
7048 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7050 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7052 this.emit( 'highlight', item
);
7059 * Fetch an item by its label.
7061 * @param {string} label Label of the item to select.
7062 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7063 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
7065 OO
.ui
.SelectWidget
.prototype.getItemFromLabel = function ( label
, prefix
) {
7067 len
= this.items
.length
,
7068 filter
= this.getItemMatcher( label
, true );
7070 for ( i
= 0; i
< len
; i
++ ) {
7071 item
= this.items
[ i
];
7072 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7079 filter
= this.getItemMatcher( label
, false );
7080 for ( i
= 0; i
< len
; i
++ ) {
7081 item
= this.items
[ i
];
7082 if ( item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() && filter( item
) ) {
7098 * Programmatically select an option by its label. If the item does not exist,
7099 * all options will be deselected.
7101 * @param {string} [label] Label of the item to select.
7102 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
7105 * @return {OO.ui.Widget} The widget, for chaining
7107 OO
.ui
.SelectWidget
.prototype.selectItemByLabel = function ( label
, prefix
) {
7108 var itemFromLabel
= this.getItemFromLabel( label
, !!prefix
);
7109 if ( label
=== undefined || !itemFromLabel
) {
7110 return this.selectItem();
7112 return this.selectItem( itemFromLabel
);
7116 * Programmatically select an option by its data. If the `data` parameter is omitted,
7117 * or if the item does not exist, all options will be deselected.
7119 * @param {Object|string} [data] Value of the item to select, omit to deselect all
7122 * @return {OO.ui.Widget} The widget, for chaining
7124 OO
.ui
.SelectWidget
.prototype.selectItemByData = function ( data
) {
7125 var itemFromData
= this.findItemFromData( data
);
7126 if ( data
=== undefined || !itemFromData
) {
7127 return this.selectItem();
7129 return this.selectItem( itemFromData
);
7133 * Programmatically select an option by its reference. If the `item` parameter is omitted,
7134 * all options will be deselected.
7136 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
7139 * @return {OO.ui.Widget} The widget, for chaining
7141 OO
.ui
.SelectWidget
.prototype.selectItem = function ( item
) {
7142 var i
, len
, selected
,
7145 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7146 selected
= this.items
[ i
] === item
;
7147 if ( this.items
[ i
].isSelected() !== selected
) {
7148 this.items
[ i
].setSelected( selected
);
7153 if ( item
&& !item
.constructor.static.highlightable
) {
7155 this.$focusOwner
.attr( 'aria-activedescendant', item
.getElementId() );
7157 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7160 this.emit( 'select', item
);
7169 * Press is a state that occurs when a user mouses down on an item, but has not
7170 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
7171 * releases the mouse.
7173 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
7176 * @return {OO.ui.Widget} The widget, for chaining
7178 OO
.ui
.SelectWidget
.prototype.pressItem = function ( item
) {
7179 var i
, len
, pressed
,
7182 for ( i
= 0, len
= this.items
.length
; i
< len
; i
++ ) {
7183 pressed
= this.items
[ i
] === item
;
7184 if ( this.items
[ i
].isPressed() !== pressed
) {
7185 this.items
[ i
].setPressed( pressed
);
7190 this.emit( 'press', item
);
7199 * Note that ‘choose’ should never be modified programmatically. A user can choose
7200 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
7201 * use the #selectItem method.
7203 * This method is identical to #selectItem, but may vary in subclasses that take additional action
7204 * when users choose an item with the keyboard or mouse.
7206 * @param {OO.ui.OptionWidget} item Item to choose
7209 * @return {OO.ui.Widget} The widget, for chaining
7211 OO
.ui
.SelectWidget
.prototype.chooseItem = function ( item
) {
7213 this.selectItem( item
);
7214 this.emit( 'choose', item
);
7221 * Find an option by its position relative to the specified item (or to the start of the option array,
7222 * if item is `null`). The direction in which to search through the option array is specified with a
7223 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
7224 * `null` if there are no options in the array.
7226 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
7227 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
7228 * @param {Function} [filter] Only consider items for which this function returns
7229 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
7230 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
7232 OO
.ui
.SelectWidget
.prototype.findRelativeSelectableItem = function ( item
, direction
, filter
) {
7233 var currentIndex
, nextIndex
, i
,
7234 increase
= direction
> 0 ? 1 : -1,
7235 len
= this.items
.length
;
7237 if ( item
instanceof OO
.ui
.OptionWidget
) {
7238 currentIndex
= this.items
.indexOf( item
);
7239 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
7241 // If no item is selected and moving forward, start at the beginning.
7242 // If moving backward, start at the end.
7243 nextIndex
= direction
> 0 ? 0 : len
- 1;
7246 for ( i
= 0; i
< len
; i
++ ) {
7247 item
= this.items
[ nextIndex
];
7249 item
instanceof OO
.ui
.OptionWidget
&& item
.isSelectable() &&
7250 ( !filter
|| filter( item
) )
7254 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
7260 * Find the next selectable item or `null` if there are no selectable items.
7261 * Disabled options and menu-section markers and breaks are not selectable.
7263 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
7265 OO
.ui
.SelectWidget
.prototype.findFirstSelectableItem = function () {
7266 return this.findRelativeSelectableItem( null, 1 );
7270 * Add an array of options to the select. Optionally, an index number can be used to
7271 * specify an insertion point.
7273 * @param {OO.ui.OptionWidget[]} items Items to add
7274 * @param {number} [index] Index to insert items after
7277 * @return {OO.ui.Widget} The widget, for chaining
7279 OO
.ui
.SelectWidget
.prototype.addItems = function ( items
, index
) {
7281 OO
.ui
.mixin
.GroupWidget
.prototype.addItems
.call( this, items
, index
);
7283 // Always provide an index, even if it was omitted
7284 this.emit( 'add', items
, index
=== undefined ? this.items
.length
- items
.length
- 1 : index
);
7290 * Remove the specified array of options from the select. Options will be detached
7291 * from the DOM, not removed, so they can be reused later. To remove all options from
7292 * the select, you may wish to use the #clearItems method instead.
7294 * @param {OO.ui.OptionWidget[]} items Items to remove
7297 * @return {OO.ui.Widget} The widget, for chaining
7299 OO
.ui
.SelectWidget
.prototype.removeItems = function ( items
) {
7302 // Deselect items being removed
7303 for ( i
= 0, len
= items
.length
; i
< len
; i
++ ) {
7305 if ( item
.isSelected() ) {
7306 this.selectItem( null );
7311 OO
.ui
.mixin
.GroupWidget
.prototype.removeItems
.call( this, items
);
7313 this.emit( 'remove', items
);
7319 * Clear all options from the select. Options will be detached from the DOM, not removed,
7320 * so that they can be reused later. To remove a subset of options from the select, use
7321 * the #removeItems method.
7325 * @return {OO.ui.Widget} The widget, for chaining
7327 OO
.ui
.SelectWidget
.prototype.clearItems = function () {
7328 var items
= this.items
.slice();
7331 OO
.ui
.mixin
.GroupWidget
.prototype.clearItems
.call( this );
7334 this.selectItem( null );
7336 this.emit( 'remove', items
);
7342 * Set the DOM element which has focus while the user is interacting with this SelectWidget.
7344 * This is used to set `aria-activedescendant` and `aria-expanded` on it.
7347 * @param {jQuery} $focusOwner
7349 OO
.ui
.SelectWidget
.prototype.setFocusOwner = function ( $focusOwner
) {
7350 this.$focusOwner
= $focusOwner
;
7354 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
7355 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
7356 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
7357 * options. For more information about options and selects, please see the
7358 * [OOUI documentation on MediaWiki][1].
7361 * // Decorated options in a select widget.
7362 * var select = new OO.ui.SelectWidget( {
7364 * new OO.ui.DecoratedOptionWidget( {
7366 * label: 'Option with icon',
7369 * new OO.ui.DecoratedOptionWidget( {
7371 * label: 'Option with indicator',
7376 * $( document.body ).append( select.$element );
7378 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7381 * @extends OO.ui.OptionWidget
7382 * @mixins OO.ui.mixin.IconElement
7383 * @mixins OO.ui.mixin.IndicatorElement
7386 * @param {Object} [config] Configuration options
7388 OO
.ui
.DecoratedOptionWidget
= function OoUiDecoratedOptionWidget( config
) {
7389 // Parent constructor
7390 OO
.ui
.DecoratedOptionWidget
.parent
.call( this, config
);
7392 // Mixin constructors
7393 OO
.ui
.mixin
.IconElement
.call( this, config
);
7394 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
7398 .addClass( 'oo-ui-decoratedOptionWidget' )
7399 .prepend( this.$icon
)
7400 .append( this.$indicator
);
7405 OO
.inheritClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.OptionWidget
);
7406 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IconElement
);
7407 OO
.mixinClass( OO
.ui
.DecoratedOptionWidget
, OO
.ui
.mixin
.IndicatorElement
);
7410 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
7411 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
7412 * the [OOUI documentation on MediaWiki] [1] for more information.
7414 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
7417 * @extends OO.ui.DecoratedOptionWidget
7420 * @param {Object} [config] Configuration options
7422 OO
.ui
.MenuOptionWidget
= function OoUiMenuOptionWidget( config
) {
7423 // Parent constructor
7424 OO
.ui
.MenuOptionWidget
.parent
.call( this, config
);
7427 this.checkIcon
= new OO
.ui
.IconWidget( {
7429 classes
: [ 'oo-ui-menuOptionWidget-checkIcon' ]
7434 .prepend( this.checkIcon
.$element
)
7435 .addClass( 'oo-ui-menuOptionWidget' );
7440 OO
.inheritClass( OO
.ui
.MenuOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7442 /* Static Properties */
7448 OO
.ui
.MenuOptionWidget
.static.scrollIntoViewOnSelect
= true;
7451 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
7452 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
7455 * var dropdown = new OO.ui.DropdownWidget( {
7458 * new OO.ui.MenuSectionOptionWidget( {
7461 * new OO.ui.MenuOptionWidget( {
7463 * label: 'Welsh Corgi'
7465 * new OO.ui.MenuOptionWidget( {
7467 * label: 'Standard Poodle'
7469 * new OO.ui.MenuSectionOptionWidget( {
7472 * new OO.ui.MenuOptionWidget( {
7479 * $( document.body ).append( dropdown.$element );
7482 * @extends OO.ui.DecoratedOptionWidget
7485 * @param {Object} [config] Configuration options
7487 OO
.ui
.MenuSectionOptionWidget
= function OoUiMenuSectionOptionWidget( config
) {
7488 // Parent constructor
7489 OO
.ui
.MenuSectionOptionWidget
.parent
.call( this, config
);
7492 this.$element
.addClass( 'oo-ui-menuSectionOptionWidget' )
7493 .removeAttr( 'role aria-selected' );
7498 OO
.inheritClass( OO
.ui
.MenuSectionOptionWidget
, OO
.ui
.DecoratedOptionWidget
);
7500 /* Static Properties */
7506 OO
.ui
.MenuSectionOptionWidget
.static.selectable
= false;
7512 OO
.ui
.MenuSectionOptionWidget
.static.highlightable
= false;
7515 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
7516 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
7517 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
7518 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
7519 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
7520 * and customized to be opened, closed, and displayed as needed.
7522 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
7523 * mouse outside the menu.
7525 * Menus also have support for keyboard interaction:
7527 * - Enter/Return key: choose and select a menu option
7528 * - Up-arrow key: highlight the previous menu option
7529 * - Down-arrow key: highlight the next menu option
7530 * - Esc key: hide the menu
7532 * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
7534 * Please see the [OOUI documentation on MediaWiki][1] for more information.
7535 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
7538 * @extends OO.ui.SelectWidget
7539 * @mixins OO.ui.mixin.ClippableElement
7540 * @mixins OO.ui.mixin.FloatableElement
7543 * @param {Object} [config] Configuration options
7544 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
7545 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
7546 * and {@link OO.ui.mixin.LookupElement LookupElement}
7547 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
7548 * the text the user types. This config is used by {@link OO.ui.TagMultiselectWidget TagMultiselectWidget}
7549 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
7550 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
7551 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
7552 * that button, unless the button (or its parent widget) is passed in here.
7553 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
7554 * @cfg {jQuery} [$autoCloseIgnore] If these elements are clicked, don't auto-hide the menu.
7555 * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
7556 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
7557 * @cfg {boolean} [highlightOnFilter] Highlight the first result when filtering
7558 * @cfg {number} [width] Width of the menu
7560 OO
.ui
.MenuSelectWidget
= function OoUiMenuSelectWidget( config
) {
7561 // Configuration initialization
7562 config
= config
|| {};
7564 // Parent constructor
7565 OO
.ui
.MenuSelectWidget
.parent
.call( this, config
);
7567 // Mixin constructors
7568 OO
.ui
.mixin
.ClippableElement
.call( this, $.extend( {}, config
, { $clippable
: this.$group
} ) );
7569 OO
.ui
.mixin
.FloatableElement
.call( this, config
);
7571 // Initial vertical positions other than 'center' will result in
7572 // the menu being flipped if there is not enough space in the container.
7573 // Store the original position so we know what to reset to.
7574 this.originalVerticalPosition
= this.verticalPosition
;
7577 this.autoHide
= config
.autoHide
=== undefined || !!config
.autoHide
;
7578 this.hideOnChoose
= config
.hideOnChoose
=== undefined || !!config
.hideOnChoose
;
7579 this.filterFromInput
= !!config
.filterFromInput
;
7580 this.$input
= config
.$input
? config
.$input
: config
.input
? config
.input
.$input
: null;
7581 this.$widget
= config
.widget
? config
.widget
.$element
: null;
7582 this.$autoCloseIgnore
= config
.$autoCloseIgnore
|| $( [] );
7583 this.onDocumentMouseDownHandler
= this.onDocumentMouseDown
.bind( this );
7584 this.onInputEditHandler
= OO
.ui
.debounce( this.updateItemVisibility
.bind( this ), 100 );
7585 this.highlightOnFilter
= !!config
.highlightOnFilter
;
7586 this.width
= config
.width
;
7589 this.$element
.addClass( 'oo-ui-menuSelectWidget' );
7590 if ( config
.widget
) {
7591 this.setFocusOwner( config
.widget
.$tabIndexed
);
7594 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
7595 // that reference properties not initialized at that time of parent class construction
7596 // TODO: Find a better way to handle post-constructor setup
7597 this.visible
= false;
7598 this.$element
.addClass( 'oo-ui-element-hidden' );
7599 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7604 OO
.inheritClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.SelectWidget
);
7605 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.ClippableElement
);
7606 OO
.mixinClass( OO
.ui
.MenuSelectWidget
, OO
.ui
.mixin
.FloatableElement
);
7613 * The menu is ready: it is visible and has been positioned and clipped.
7616 /* Static properties */
7619 * Positions to flip to if there isn't room in the container for the
7620 * menu in a specific direction.
7622 * @property {Object.<string,string>}
7624 OO
.ui
.MenuSelectWidget
.static.flippedPositions
= {
7634 * Handles document mouse down events.
7637 * @param {MouseEvent} e Mouse down event
7639 OO
.ui
.MenuSelectWidget
.prototype.onDocumentMouseDown = function ( e
) {
7643 this.$element
.add( this.$widget
).add( this.$autoCloseIgnore
).get(),
7648 this.toggle( false );
7655 OO
.ui
.MenuSelectWidget
.prototype.onDocumentKeyDown = function ( e
) {
7656 var currentItem
= this.findHighlightedItem() || this.findSelectedItem();
7658 if ( !this.isDisabled() && this.isVisible() ) {
7659 switch ( e
.keyCode
) {
7660 case OO
.ui
.Keys
.LEFT
:
7661 case OO
.ui
.Keys
.RIGHT
:
7662 // Do nothing if a text field is associated, arrow keys will be handled natively
7663 if ( !this.$input
) {
7664 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7667 case OO
.ui
.Keys
.ESCAPE
:
7668 case OO
.ui
.Keys
.TAB
:
7669 if ( currentItem
) {
7670 currentItem
.setHighlighted( false );
7672 this.toggle( false );
7673 // Don't prevent tabbing away, prevent defocusing
7674 if ( e
.keyCode
=== OO
.ui
.Keys
.ESCAPE
) {
7676 e
.stopPropagation();
7680 OO
.ui
.MenuSelectWidget
.parent
.prototype.onDocumentKeyDown
.call( this, e
);
7687 * Update menu item visibility and clipping after input changes (if filterFromInput is enabled)
7688 * or after items were added/removed (always).
7692 OO
.ui
.MenuSelectWidget
.prototype.updateItemVisibility = function () {
7693 var i
, item
, items
, visible
, section
, sectionEmpty
, filter
, exactFilter
,
7695 len
= this.items
.length
,
7696 showAll
= !this.isVisible(),
7699 if ( this.$input
&& this.filterFromInput
) {
7700 filter
= showAll
? null : this.getItemMatcher( this.$input
.val() );
7701 exactFilter
= this.getItemMatcher( this.$input
.val(), true );
7702 // Hide non-matching options, and also hide section headers if all options
7703 // in their section are hidden.
7704 for ( i
= 0; i
< len
; i
++ ) {
7705 item
= this.items
[ i
];
7706 if ( item
instanceof OO
.ui
.MenuSectionOptionWidget
) {
7708 // If the previous section was empty, hide its header
7709 section
.toggle( showAll
|| !sectionEmpty
);
7712 sectionEmpty
= true;
7713 } else if ( item
instanceof OO
.ui
.OptionWidget
) {
7714 visible
= showAll
|| filter( item
);
7715 exactMatch
= exactMatch
|| exactFilter( item
);
7716 anyVisible
= anyVisible
|| visible
;
7717 sectionEmpty
= sectionEmpty
&& !visible
;
7718 item
.toggle( visible
);
7721 // Process the final section
7723 section
.toggle( showAll
|| !sectionEmpty
);
7726 if ( anyVisible
&& this.items
.length
&& !exactMatch
) {
7727 this.scrollItemIntoView( this.items
[ 0 ] );
7730 if ( !anyVisible
) {
7731 this.highlightItem( null );
7734 this.$element
.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible
);
7736 if ( this.highlightOnFilter
) {
7737 // Highlight the first item on the list
7739 items
= this.getItems();
7740 for ( i
= 0; i
< items
.length
; i
++ ) {
7741 if ( items
[ i
].isVisible() ) {
7746 this.highlightItem( item
);
7751 // Reevaluate clipping
7758 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyDownListener = function () {
7759 if ( this.$input
) {
7760 this.$input
.on( 'keydown', this.onDocumentKeyDownHandler
);
7762 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyDownListener
.call( this );
7769 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyDownListener = function () {
7770 if ( this.$input
) {
7771 this.$input
.off( 'keydown', this.onDocumentKeyDownHandler
);
7773 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyDownListener
.call( this );
7780 OO
.ui
.MenuSelectWidget
.prototype.bindDocumentKeyPressListener = function () {
7781 if ( this.$input
) {
7782 if ( this.filterFromInput
) {
7783 this.$input
.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7784 this.updateItemVisibility();
7787 OO
.ui
.MenuSelectWidget
.parent
.prototype.bindDocumentKeyPressListener
.call( this );
7794 OO
.ui
.MenuSelectWidget
.prototype.unbindDocumentKeyPressListener = function () {
7795 if ( this.$input
) {
7796 if ( this.filterFromInput
) {
7797 this.$input
.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler
);
7798 this.updateItemVisibility();
7801 OO
.ui
.MenuSelectWidget
.parent
.prototype.unbindDocumentKeyPressListener
.call( this );
7808 * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
7810 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
7811 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
7813 * @param {OO.ui.OptionWidget} item Item to choose
7815 * @return {OO.ui.Widget} The widget, for chaining
7817 OO
.ui
.MenuSelectWidget
.prototype.chooseItem = function ( item
) {
7818 OO
.ui
.MenuSelectWidget
.parent
.prototype.chooseItem
.call( this, item
);
7819 if ( this.hideOnChoose
) {
7820 this.toggle( false );
7828 OO
.ui
.MenuSelectWidget
.prototype.addItems = function ( items
, index
) {
7830 OO
.ui
.MenuSelectWidget
.parent
.prototype.addItems
.call( this, items
, index
);
7832 this.updateItemVisibility();
7840 OO
.ui
.MenuSelectWidget
.prototype.removeItems = function ( items
) {
7842 OO
.ui
.MenuSelectWidget
.parent
.prototype.removeItems
.call( this, items
);
7844 this.updateItemVisibility();
7852 OO
.ui
.MenuSelectWidget
.prototype.clearItems = function () {
7854 OO
.ui
.MenuSelectWidget
.parent
.prototype.clearItems
.call( this );
7856 this.updateItemVisibility();
7862 * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
7863 * `.toggle( true )` after its #$element is attached to the DOM.
7865 * Do not show the menu while it is not attached to the DOM. The calculations required to display
7866 * it in the right place and with the right dimensions only work correctly while it is attached.
7867 * Side-effects may include broken interface and exceptions being thrown. This wasn't always
7868 * strictly enforced, so currently it only generates a warning in the browser console.
7873 OO
.ui
.MenuSelectWidget
.prototype.toggle = function ( visible
) {
7874 var change
, originalHeight
, flippedHeight
;
7876 visible
= ( visible
=== undefined ? !this.visible
: !!visible
) && !!this.items
.length
;
7877 change
= visible
!== this.isVisible();
7879 if ( visible
&& !this.warnedUnattached
&& !this.isElementAttached() ) {
7880 OO
.ui
.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
7881 this.warnedUnattached
= true;
7884 if ( change
&& visible
) {
7885 // Reset position before showing the popup again. It's possible we no longer need to flip
7886 // (e.g. if the user scrolled).
7887 this.setVerticalPosition( this.originalVerticalPosition
);
7891 OO
.ui
.MenuSelectWidget
.parent
.prototype.toggle
.call( this, visible
);
7897 this.setIdealSize( this.width
);
7898 } else if ( this.$floatableContainer
) {
7899 this.$clippable
.css( 'width', 'auto' );
7901 this.$floatableContainer
[ 0 ].offsetWidth
> this.$clippable
[ 0 ].offsetWidth
?
7902 // Dropdown is smaller than handle so expand to width
7903 this.$floatableContainer
[ 0 ].offsetWidth
:
7904 // Dropdown is larger than handle so auto size
7907 this.$clippable
.css( 'width', '' );
7910 this.togglePositioning( !!this.$floatableContainer
);
7911 this.toggleClipping( true );
7913 this.bindDocumentKeyDownListener();
7914 this.bindDocumentKeyPressListener();
7917 ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
7918 this.originalVerticalPosition
!== 'center'
7920 // If opening the menu in one direction causes it to be clipped, flip it
7921 originalHeight
= this.$element
.height();
7922 this.setVerticalPosition(
7923 this.constructor.static.flippedPositions
[ this.originalVerticalPosition
]
7925 if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
7926 // If flipping also causes it to be clipped, open in whichever direction
7927 // we have more space
7928 flippedHeight
= this.$element
.height();
7929 if ( originalHeight
> flippedHeight
) {
7930 this.setVerticalPosition( this.originalVerticalPosition
);
7934 // Note that we do not flip the menu's opening direction if the clipping changes
7935 // later (e.g. after the user scrolls), that seems like it would be annoying
7937 this.$focusOwner
.attr( 'aria-expanded', 'true' );
7939 if ( this.findSelectedItem() ) {
7940 this.$focusOwner
.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() );
7941 this.findSelectedItem().scrollElementIntoView( { duration
: 0 } );
7945 if ( this.autoHide
) {
7946 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7949 this.emit( 'ready' );
7951 this.$focusOwner
.removeAttr( 'aria-activedescendant' );
7952 this.unbindDocumentKeyDownListener();
7953 this.unbindDocumentKeyPressListener();
7954 this.$focusOwner
.attr( 'aria-expanded', 'false' );
7955 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler
, true );
7956 this.togglePositioning( false );
7957 this.toggleClipping( false );
7965 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
7966 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
7967 * users can interact with it.
7969 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
7970 * OO.ui.DropdownInputWidget instead.
7973 * // A DropdownWidget with a menu that contains three options.
7974 * var dropDown = new OO.ui.DropdownWidget( {
7975 * label: 'Dropdown menu: Select a menu option',
7978 * new OO.ui.MenuOptionWidget( {
7982 * new OO.ui.MenuOptionWidget( {
7986 * new OO.ui.MenuOptionWidget( {
7994 * $( document.body ).append( dropDown.$element );
7996 * dropDown.getMenu().selectItemByData( 'b' );
7998 * dropDown.getMenu().findSelectedItem().getData(); // Returns 'b'.
8000 * For more information, please see the [OOUI documentation on MediaWiki] [1].
8002 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8005 * @extends OO.ui.Widget
8006 * @mixins OO.ui.mixin.IconElement
8007 * @mixins OO.ui.mixin.IndicatorElement
8008 * @mixins OO.ui.mixin.LabelElement
8009 * @mixins OO.ui.mixin.TitledElement
8010 * @mixins OO.ui.mixin.TabIndexedElement
8013 * @param {Object} [config] Configuration options
8014 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.MenuSelectWidget menu select widget}
8015 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
8016 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
8017 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
8018 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
8020 OO
.ui
.DropdownWidget
= function OoUiDropdownWidget( config
) {
8021 // Configuration initialization
8022 config
= $.extend( { indicator
: 'down' }, config
);
8024 // Parent constructor
8025 OO
.ui
.DropdownWidget
.parent
.call( this, config
);
8027 // Properties (must be set before TabIndexedElement constructor call)
8028 this.$handle
= $( '<button>' );
8029 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
8031 // Mixin constructors
8032 OO
.ui
.mixin
.IconElement
.call( this, config
);
8033 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
8034 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8035 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
8036 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$handle
} ) );
8039 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend( {
8041 $floatableContainer
: this.$element
8046 click
: this.onClick
.bind( this ),
8047 keydown
: this.onKeyDown
.bind( this ),
8048 // Hack? Handle type-to-search when menu is not expanded and not handling its own events
8049 keypress
: this.menu
.onDocumentKeyPressHandler
,
8050 blur
: this.menu
.clearKeyPressBuffer
.bind( this.menu
)
8052 this.menu
.connect( this, {
8053 select
: 'onMenuSelect',
8054 toggle
: 'onMenuToggle'
8059 .addClass( 'oo-ui-dropdownWidget-handle' )
8062 'aria-owns': this.menu
.getElementId(),
8063 'aria-haspopup': 'listbox'
8065 .append( this.$icon
, this.$label
, this.$indicator
);
8067 .addClass( 'oo-ui-dropdownWidget' )
8068 .append( this.$handle
);
8069 this.$overlay
.append( this.menu
.$element
);
8074 OO
.inheritClass( OO
.ui
.DropdownWidget
, OO
.ui
.Widget
);
8075 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IconElement
);
8076 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.IndicatorElement
);
8077 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.LabelElement
);
8078 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TitledElement
);
8079 OO
.mixinClass( OO
.ui
.DropdownWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8086 * @return {OO.ui.MenuSelectWidget} Menu of widget
8088 OO
.ui
.DropdownWidget
.prototype.getMenu = function () {
8093 * Handles menu select events.
8096 * @param {OO.ui.MenuOptionWidget} item Selected menu item
8098 OO
.ui
.DropdownWidget
.prototype.onMenuSelect = function ( item
) {
8102 this.setLabel( null );
8106 selectedLabel
= item
.getLabel();
8108 // If the label is a DOM element, clone it, because setLabel will append() it
8109 if ( selectedLabel
instanceof $ ) {
8110 selectedLabel
= selectedLabel
.clone();
8113 this.setLabel( selectedLabel
);
8117 * Handle menu toggle events.
8120 * @param {boolean} isVisible Open state of the menu
8122 OO
.ui
.DropdownWidget
.prototype.onMenuToggle = function ( isVisible
) {
8123 this.$element
.toggleClass( 'oo-ui-dropdownWidget-open', isVisible
);
8127 * Handle mouse click events.
8130 * @param {jQuery.Event} e Mouse click event
8131 * @return {undefined/boolean} False to prevent default if event is handled
8133 OO
.ui
.DropdownWidget
.prototype.onClick = function ( e
) {
8134 if ( !this.isDisabled() && e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
8141 * Handle key down events.
8144 * @param {jQuery.Event} e Key down event
8145 * @return {undefined/boolean} False to prevent default if event is handled
8147 OO
.ui
.DropdownWidget
.prototype.onKeyDown = function ( e
) {
8149 !this.isDisabled() &&
8151 e
.which
=== OO
.ui
.Keys
.ENTER
||
8153 e
.which
=== OO
.ui
.Keys
.SPACE
&&
8154 // Avoid conflicts with type-to-search, see SelectWidget#onKeyPress.
8155 // Space only closes the menu is the user is not typing to search.
8156 this.menu
.keyPressBuffer
=== ''
8159 !this.menu
.isVisible() &&
8161 e
.which
=== OO
.ui
.Keys
.UP
||
8162 e
.which
=== OO
.ui
.Keys
.DOWN
8173 * RadioOptionWidget is an option widget that looks like a radio button.
8174 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
8175 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8177 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8180 * @extends OO.ui.OptionWidget
8183 * @param {Object} [config] Configuration options
8185 OO
.ui
.RadioOptionWidget
= function OoUiRadioOptionWidget( config
) {
8186 // Configuration initialization
8187 config
= config
|| {};
8189 // Properties (must be done before parent constructor which calls #setDisabled)
8190 this.radio
= new OO
.ui
.RadioInputWidget( { value
: config
.data
, tabIndex
: -1 } );
8192 // Parent constructor
8193 OO
.ui
.RadioOptionWidget
.parent
.call( this, config
);
8196 // Remove implicit role, we're handling it ourselves
8197 this.radio
.$input
.attr( 'role', 'presentation' );
8199 .addClass( 'oo-ui-radioOptionWidget' )
8200 .attr( 'role', 'radio' )
8201 .attr( 'aria-checked', 'false' )
8202 .removeAttr( 'aria-selected' )
8203 .prepend( this.radio
.$element
);
8208 OO
.inheritClass( OO
.ui
.RadioOptionWidget
, OO
.ui
.OptionWidget
);
8210 /* Static Properties */
8216 OO
.ui
.RadioOptionWidget
.static.highlightable
= false;
8222 OO
.ui
.RadioOptionWidget
.static.scrollIntoViewOnSelect
= true;
8228 OO
.ui
.RadioOptionWidget
.static.pressable
= false;
8234 OO
.ui
.RadioOptionWidget
.static.tagName
= 'label';
8241 OO
.ui
.RadioOptionWidget
.prototype.setSelected = function ( state
) {
8242 OO
.ui
.RadioOptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8244 this.radio
.setSelected( state
);
8246 .attr( 'aria-checked', state
.toString() )
8247 .removeAttr( 'aria-selected' );
8255 OO
.ui
.RadioOptionWidget
.prototype.setDisabled = function ( disabled
) {
8256 OO
.ui
.RadioOptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8258 this.radio
.setDisabled( this.isDisabled() );
8264 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
8265 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
8266 * an interface for adding, removing and selecting options.
8267 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8269 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8270 * OO.ui.RadioSelectInputWidget instead.
8273 * // A RadioSelectWidget with RadioOptions.
8274 * var option1 = new OO.ui.RadioOptionWidget( {
8276 * label: 'Selected radio option'
8278 * option2 = new OO.ui.RadioOptionWidget( {
8280 * label: 'Unselected radio option'
8282 * radioSelect = new OO.ui.RadioSelectWidget( {
8283 * items: [ option1, option2 ]
8286 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
8287 * radioSelect.selectItem( option1 );
8289 * $( document.body ).append( radioSelect.$element );
8291 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8295 * @extends OO.ui.SelectWidget
8296 * @mixins OO.ui.mixin.TabIndexedElement
8299 * @param {Object} [config] Configuration options
8301 OO
.ui
.RadioSelectWidget
= function OoUiRadioSelectWidget( config
) {
8302 // Parent constructor
8303 OO
.ui
.RadioSelectWidget
.parent
.call( this, config
);
8305 // Mixin constructors
8306 OO
.ui
.mixin
.TabIndexedElement
.call( this, config
);
8310 focus
: this.bindDocumentKeyDownListener
.bind( this ),
8311 blur
: this.unbindDocumentKeyDownListener
.bind( this )
8316 .addClass( 'oo-ui-radioSelectWidget' )
8317 .attr( 'role', 'radiogroup' );
8322 OO
.inheritClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.SelectWidget
);
8323 OO
.mixinClass( OO
.ui
.RadioSelectWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8326 * MultioptionWidgets are special elements that can be selected and configured with data. The
8327 * data is often unique for each option, but it does not have to be. MultioptionWidgets are used
8328 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
8329 * and examples, please see the [OOUI documentation on MediaWiki][1].
8331 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Multioptions
8334 * @extends OO.ui.Widget
8335 * @mixins OO.ui.mixin.ItemWidget
8336 * @mixins OO.ui.mixin.LabelElement
8337 * @mixins OO.ui.mixin.TitledElement
8340 * @param {Object} [config] Configuration options
8341 * @cfg {boolean} [selected=false] Whether the option is initially selected
8343 OO
.ui
.MultioptionWidget
= function OoUiMultioptionWidget( config
) {
8344 // Configuration initialization
8345 config
= config
|| {};
8347 // Parent constructor
8348 OO
.ui
.MultioptionWidget
.parent
.call( this, config
);
8350 // Mixin constructors
8351 OO
.ui
.mixin
.ItemWidget
.call( this );
8352 OO
.ui
.mixin
.LabelElement
.call( this, config
);
8353 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8356 this.selected
= null;
8360 .addClass( 'oo-ui-multioptionWidget' )
8361 .append( this.$label
);
8362 this.setSelected( config
.selected
);
8367 OO
.inheritClass( OO
.ui
.MultioptionWidget
, OO
.ui
.Widget
);
8368 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.ItemWidget
);
8369 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.LabelElement
);
8370 OO
.mixinClass( OO
.ui
.MultioptionWidget
, OO
.ui
.mixin
.TitledElement
);
8377 * A change event is emitted when the selected state of the option changes.
8379 * @param {boolean} selected Whether the option is now selected
8385 * Check if the option is selected.
8387 * @return {boolean} Item is selected
8389 OO
.ui
.MultioptionWidget
.prototype.isSelected = function () {
8390 return this.selected
;
8394 * Set the option’s selected state. In general, all modifications to the selection
8395 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
8396 * method instead of this method.
8398 * @param {boolean} [state=false] Select option
8400 * @return {OO.ui.Widget} The widget, for chaining
8402 OO
.ui
.MultioptionWidget
.prototype.setSelected = function ( state
) {
8404 if ( this.selected
!== state
) {
8405 this.selected
= state
;
8406 this.emit( 'change', state
);
8407 this.$element
.toggleClass( 'oo-ui-multioptionWidget-selected', state
);
8413 * MultiselectWidget allows selecting multiple options from a list.
8415 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
8417 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
8421 * @extends OO.ui.Widget
8422 * @mixins OO.ui.mixin.GroupWidget
8423 * @mixins OO.ui.mixin.TitledElement
8426 * @param {Object} [config] Configuration options
8427 * @cfg {OO.ui.MultioptionWidget[]} [items] An array of options to add to the multiselect.
8429 OO
.ui
.MultiselectWidget
= function OoUiMultiselectWidget( config
) {
8430 // Parent constructor
8431 OO
.ui
.MultiselectWidget
.parent
.call( this, config
);
8433 // Configuration initialization
8434 config
= config
|| {};
8436 // Mixin constructors
8437 OO
.ui
.mixin
.GroupWidget
.call( this, config
);
8438 OO
.ui
.mixin
.TitledElement
.call( this, config
);
8441 this.aggregate( { change
: 'select' } );
8442 // This is mostly for compatibility with TagMultiselectWidget... normally, 'change' is emitted
8443 // by GroupElement only when items are added/removed
8444 this.connect( this, { select
: [ 'emit', 'change' ] } );
8447 if ( config
.items
) {
8448 this.addItems( config
.items
);
8450 this.$group
.addClass( 'oo-ui-multiselectWidget-group' );
8451 this.$element
.addClass( 'oo-ui-multiselectWidget' )
8452 .append( this.$group
);
8457 OO
.inheritClass( OO
.ui
.MultiselectWidget
, OO
.ui
.Widget
);
8458 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.GroupWidget
);
8459 OO
.mixinClass( OO
.ui
.MultiselectWidget
, OO
.ui
.mixin
.TitledElement
);
8466 * A change event is emitted when the set of items changes, or an item is selected or deselected.
8472 * A select event is emitted when an item is selected or deselected.
8478 * Find options that are selected.
8480 * @return {OO.ui.MultioptionWidget[]} Selected options
8482 OO
.ui
.MultiselectWidget
.prototype.findSelectedItems = function () {
8483 return this.items
.filter( function ( item
) {
8484 return item
.isSelected();
8489 * Find the data of options that are selected.
8491 * @return {Object[]|string[]} Values of selected options
8493 OO
.ui
.MultiselectWidget
.prototype.findSelectedItemsData = function () {
8494 return this.findSelectedItems().map( function ( item
) {
8500 * Select options by reference. Options not mentioned in the `items` array will be deselected.
8502 * @param {OO.ui.MultioptionWidget[]} items Items to select
8504 * @return {OO.ui.Widget} The widget, for chaining
8506 OO
.ui
.MultiselectWidget
.prototype.selectItems = function ( items
) {
8507 this.items
.forEach( function ( item
) {
8508 var selected
= items
.indexOf( item
) !== -1;
8509 item
.setSelected( selected
);
8515 * Select items by their data. Options not mentioned in the `datas` array will be deselected.
8517 * @param {Object[]|string[]} datas Values of items to select
8519 * @return {OO.ui.Widget} The widget, for chaining
8521 OO
.ui
.MultiselectWidget
.prototype.selectItemsByData = function ( datas
) {
8524 items
= datas
.map( function ( data
) {
8525 return widget
.findItemFromData( data
);
8527 this.selectItems( items
);
8532 * CheckboxMultioptionWidget is an option widget that looks like a checkbox.
8533 * The class is used with OO.ui.CheckboxMultiselectWidget to create a selection of checkbox options.
8534 * Please see the [OOUI documentation on MediaWiki] [1] for more information.
8536 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Button_selects_and_option
8539 * @extends OO.ui.MultioptionWidget
8542 * @param {Object} [config] Configuration options
8544 OO
.ui
.CheckboxMultioptionWidget
= function OoUiCheckboxMultioptionWidget( config
) {
8545 // Configuration initialization
8546 config
= config
|| {};
8548 // Properties (must be done before parent constructor which calls #setDisabled)
8549 this.checkbox
= new OO
.ui
.CheckboxInputWidget();
8551 // Parent constructor
8552 OO
.ui
.CheckboxMultioptionWidget
.parent
.call( this, config
);
8555 this.checkbox
.on( 'change', this.onCheckboxChange
.bind( this ) );
8556 this.$element
.on( 'keydown', this.onKeyDown
.bind( this ) );
8560 .addClass( 'oo-ui-checkboxMultioptionWidget' )
8561 .prepend( this.checkbox
.$element
);
8566 OO
.inheritClass( OO
.ui
.CheckboxMultioptionWidget
, OO
.ui
.MultioptionWidget
);
8568 /* Static Properties */
8574 OO
.ui
.CheckboxMultioptionWidget
.static.tagName
= 'label';
8579 * Handle checkbox selected state change.
8583 OO
.ui
.CheckboxMultioptionWidget
.prototype.onCheckboxChange = function () {
8584 this.setSelected( this.checkbox
.isSelected() );
8590 OO
.ui
.CheckboxMultioptionWidget
.prototype.setSelected = function ( state
) {
8591 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setSelected
.call( this, state
);
8592 this.checkbox
.setSelected( state
);
8599 OO
.ui
.CheckboxMultioptionWidget
.prototype.setDisabled = function ( disabled
) {
8600 OO
.ui
.CheckboxMultioptionWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
8601 this.checkbox
.setDisabled( this.isDisabled() );
8608 OO
.ui
.CheckboxMultioptionWidget
.prototype.focus = function () {
8609 this.checkbox
.focus();
8613 * Handle key down events.
8616 * @param {jQuery.Event} e
8618 OO
.ui
.CheckboxMultioptionWidget
.prototype.onKeyDown = function ( e
) {
8620 element
= this.getElementGroup(),
8623 if ( e
.keyCode
=== OO
.ui
.Keys
.LEFT
|| e
.keyCode
=== OO
.ui
.Keys
.UP
) {
8624 nextItem
= element
.getRelativeFocusableItem( this, -1 );
8625 } else if ( e
.keyCode
=== OO
.ui
.Keys
.RIGHT
|| e
.keyCode
=== OO
.ui
.Keys
.DOWN
) {
8626 nextItem
= element
.getRelativeFocusableItem( this, 1 );
8636 * CheckboxMultiselectWidget is a {@link OO.ui.MultiselectWidget multiselect widget} that contains
8637 * checkboxes and is used together with OO.ui.CheckboxMultioptionWidget. The
8638 * CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
8639 * Please see the [OOUI documentation on MediaWiki][1] for more information.
8641 * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
8642 * OO.ui.CheckboxMultiselectInputWidget instead.
8645 * // A CheckboxMultiselectWidget with CheckboxMultioptions.
8646 * var option1 = new OO.ui.CheckboxMultioptionWidget( {
8649 * label: 'Selected checkbox'
8651 * option2 = new OO.ui.CheckboxMultioptionWidget( {
8653 * label: 'Unselected checkbox'
8655 * multiselect = new OO.ui.CheckboxMultiselectWidget( {
8656 * items: [ option1, option2 ]
8658 * $( document.body ).append( multiselect.$element );
8660 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
8663 * @extends OO.ui.MultiselectWidget
8666 * @param {Object} [config] Configuration options
8668 OO
.ui
.CheckboxMultiselectWidget
= function OoUiCheckboxMultiselectWidget( config
) {
8669 // Parent constructor
8670 OO
.ui
.CheckboxMultiselectWidget
.parent
.call( this, config
);
8673 this.$lastClicked
= null;
8676 this.$group
.on( 'click', this.onClick
.bind( this ) );
8680 .addClass( 'oo-ui-checkboxMultiselectWidget' );
8685 OO
.inheritClass( OO
.ui
.CheckboxMultiselectWidget
, OO
.ui
.MultiselectWidget
);
8690 * Get an option by its position relative to the specified item (or to the start of the option array,
8691 * if item is `null`). The direction in which to search through the option array is specified with a
8692 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
8693 * `null` if there are no options in the array.
8695 * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
8696 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
8697 * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
8699 OO
.ui
.CheckboxMultiselectWidget
.prototype.getRelativeFocusableItem = function ( item
, direction
) {
8700 var currentIndex
, nextIndex
, i
,
8701 increase
= direction
> 0 ? 1 : -1,
8702 len
= this.items
.length
;
8705 currentIndex
= this.items
.indexOf( item
);
8706 nextIndex
= ( currentIndex
+ increase
+ len
) % len
;
8708 // If no item is selected and moving forward, start at the beginning.
8709 // If moving backward, start at the end.
8710 nextIndex
= direction
> 0 ? 0 : len
- 1;
8713 for ( i
= 0; i
< len
; i
++ ) {
8714 item
= this.items
[ nextIndex
];
8715 if ( item
&& !item
.isDisabled() ) {
8718 nextIndex
= ( nextIndex
+ increase
+ len
) % len
;
8724 * Handle click events on checkboxes.
8726 * @param {jQuery.Event} e
8728 OO
.ui
.CheckboxMultiselectWidget
.prototype.onClick = function ( e
) {
8729 var $options
, lastClickedIndex
, nowClickedIndex
, i
, direction
, wasSelected
, items
,
8730 $lastClicked
= this.$lastClicked
,
8731 $nowClicked
= $( e
.target
).closest( '.oo-ui-checkboxMultioptionWidget' )
8732 .not( '.oo-ui-widget-disabled' );
8734 // Allow selecting multiple options at once by Shift-clicking them
8735 if ( $lastClicked
&& $nowClicked
.length
&& e
.shiftKey
) {
8736 $options
= this.$group
.find( '.oo-ui-checkboxMultioptionWidget' );
8737 lastClickedIndex
= $options
.index( $lastClicked
);
8738 nowClickedIndex
= $options
.index( $nowClicked
);
8739 // If it's the same item, either the user is being silly, or it's a fake event generated by the
8740 // browser. In either case we don't need custom handling.
8741 if ( nowClickedIndex
!== lastClickedIndex
) {
8743 wasSelected
= items
[ nowClickedIndex
].isSelected();
8744 direction
= nowClickedIndex
> lastClickedIndex
? 1 : -1;
8746 // This depends on the DOM order of the items and the order of the .items array being the same.
8747 for ( i
= lastClickedIndex
; i
!== nowClickedIndex
; i
+= direction
) {
8748 if ( !items
[ i
].isDisabled() ) {
8749 items
[ i
].setSelected( !wasSelected
);
8752 // For the now-clicked element, use immediate timeout to allow the browser to do its own
8753 // handling first, then set our value. The order in which events happen is different for
8754 // clicks on the <input> and on the <label> and there are additional fake clicks fired for
8755 // non-click actions that change the checkboxes.
8757 setTimeout( function () {
8758 if ( !items
[ nowClickedIndex
].isDisabled() ) {
8759 items
[ nowClickedIndex
].setSelected( !wasSelected
);
8765 if ( $nowClicked
.length
) {
8766 this.$lastClicked
= $nowClicked
;
8774 * @return {OO.ui.Widget} The widget, for chaining
8776 OO
.ui
.CheckboxMultiselectWidget
.prototype.focus = function () {
8778 if ( !this.isDisabled() ) {
8779 item
= this.getRelativeFocusableItem( null, 1 );
8790 OO
.ui
.CheckboxMultiselectWidget
.prototype.simulateLabelClick = function () {
8795 * Progress bars visually display the status of an operation, such as a download,
8796 * and can be either determinate or indeterminate:
8798 * - **determinate** process bars show the percent of an operation that is complete.
8800 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
8801 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
8802 * not use percentages.
8804 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
8807 * // Examples of determinate and indeterminate progress bars.
8808 * var progressBar1 = new OO.ui.ProgressBarWidget( {
8811 * var progressBar2 = new OO.ui.ProgressBarWidget();
8813 * // Create a FieldsetLayout to layout progress bars.
8814 * var fieldset = new OO.ui.FieldsetLayout;
8815 * fieldset.addItems( [
8816 * new OO.ui.FieldLayout( progressBar1, {
8817 * label: 'Determinate',
8820 * new OO.ui.FieldLayout( progressBar2, {
8821 * label: 'Indeterminate',
8825 * $( document.body ).append( fieldset.$element );
8828 * @extends OO.ui.Widget
8831 * @param {Object} [config] Configuration options
8832 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
8833 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
8834 * By default, the progress bar is indeterminate.
8836 OO
.ui
.ProgressBarWidget
= function OoUiProgressBarWidget( config
) {
8837 // Configuration initialization
8838 config
= config
|| {};
8840 // Parent constructor
8841 OO
.ui
.ProgressBarWidget
.parent
.call( this, config
);
8844 this.$bar
= $( '<div>' );
8845 this.progress
= null;
8848 this.setProgress( config
.progress
!== undefined ? config
.progress
: false );
8849 this.$bar
.addClass( 'oo-ui-progressBarWidget-bar' );
8852 role
: 'progressbar',
8854 'aria-valuemax': 100
8856 .addClass( 'oo-ui-progressBarWidget' )
8857 .append( this.$bar
);
8862 OO
.inheritClass( OO
.ui
.ProgressBarWidget
, OO
.ui
.Widget
);
8864 /* Static Properties */
8870 OO
.ui
.ProgressBarWidget
.static.tagName
= 'div';
8875 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
8877 * @return {number|boolean} Progress percent
8879 OO
.ui
.ProgressBarWidget
.prototype.getProgress = function () {
8880 return this.progress
;
8884 * Set the percent of the process completed or `false` for an indeterminate process.
8886 * @param {number|boolean} progress Progress percent or `false` for indeterminate
8888 OO
.ui
.ProgressBarWidget
.prototype.setProgress = function ( progress
) {
8889 this.progress
= progress
;
8891 if ( progress
!== false ) {
8892 this.$bar
.css( 'width', this.progress
+ '%' );
8893 this.$element
.attr( 'aria-valuenow', this.progress
);
8895 this.$bar
.css( 'width', '' );
8896 this.$element
.removeAttr( 'aria-valuenow' );
8898 this.$element
.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress
=== false );
8902 * InputWidget is the base class for all input widgets, which
8903 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
8904 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
8905 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
8907 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
8911 * @extends OO.ui.Widget
8912 * @mixins OO.ui.mixin.FlaggedElement
8913 * @mixins OO.ui.mixin.TabIndexedElement
8914 * @mixins OO.ui.mixin.TitledElement
8915 * @mixins OO.ui.mixin.AccessKeyedElement
8918 * @param {Object} [config] Configuration options
8919 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
8920 * @cfg {string} [value=''] The value of the input.
8921 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
8922 * @cfg {string} [inputId] The value of the input’s HTML `id` attribute.
8923 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
8924 * before it is accepted.
8926 OO
.ui
.InputWidget
= function OoUiInputWidget( config
) {
8927 // Configuration initialization
8928 config
= config
|| {};
8930 // Parent constructor
8931 OO
.ui
.InputWidget
.parent
.call( this, config
);
8934 // See #reusePreInfuseDOM about config.$input
8935 this.$input
= config
.$input
|| this.getInputElement( config
);
8937 this.inputFilter
= config
.inputFilter
;
8939 // Mixin constructors
8940 OO
.ui
.mixin
.FlaggedElement
.call( this, config
);
8941 OO
.ui
.mixin
.TabIndexedElement
.call( this, $.extend( {}, config
, { $tabIndexed
: this.$input
} ) );
8942 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$input
} ) );
8943 OO
.ui
.mixin
.AccessKeyedElement
.call( this, $.extend( {}, config
, { $accessKeyed
: this.$input
} ) );
8946 this.$input
.on( 'keydown mouseup cut paste change input select', this.onEdit
.bind( this ) );
8950 .addClass( 'oo-ui-inputWidget-input' )
8951 .attr( 'name', config
.name
)
8952 .prop( 'disabled', this.isDisabled() );
8954 .addClass( 'oo-ui-inputWidget' )
8955 .append( this.$input
);
8956 this.setValue( config
.value
);
8958 this.setDir( config
.dir
);
8960 if ( config
.inputId
!== undefined ) {
8961 this.setInputId( config
.inputId
);
8967 OO
.inheritClass( OO
.ui
.InputWidget
, OO
.ui
.Widget
);
8968 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.FlaggedElement
);
8969 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TabIndexedElement
);
8970 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.TitledElement
);
8971 OO
.mixinClass( OO
.ui
.InputWidget
, OO
.ui
.mixin
.AccessKeyedElement
);
8973 /* Static Methods */
8978 OO
.ui
.InputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
8979 config
= OO
.ui
.InputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
8980 // Reusing `$input` lets browsers preserve inputted values across page reloads, see T114134.
8981 config
.$input
= $( node
).find( '.oo-ui-inputWidget-input' );
8988 OO
.ui
.InputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
8989 var state
= OO
.ui
.InputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
8990 if ( config
.$input
&& config
.$input
.length
) {
8991 state
.value
= config
.$input
.val();
8992 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
8993 state
.focus
= config
.$input
.is( ':focus' );
9003 * A change event is emitted when the value of the input changes.
9005 * @param {string} value
9011 * Get input element.
9013 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
9014 * different circumstances. The element must have a `value` property (like form elements).
9017 * @param {Object} config Configuration options
9018 * @return {jQuery} Input element
9020 OO
.ui
.InputWidget
.prototype.getInputElement = function () {
9021 return $( '<input>' );
9025 * Handle potentially value-changing events.
9028 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9030 OO
.ui
.InputWidget
.prototype.onEdit = function () {
9032 if ( !this.isDisabled() ) {
9033 // Allow the stack to clear so the value will be updated
9034 setTimeout( function () {
9035 widget
.setValue( widget
.$input
.val() );
9041 * Get the value of the input.
9043 * @return {string} Input value
9045 OO
.ui
.InputWidget
.prototype.getValue = function () {
9046 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9047 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9048 var value
= this.$input
.val();
9049 if ( this.value
!== value
) {
9050 this.setValue( value
);
9056 * Set the directionality of the input.
9058 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
9060 * @return {OO.ui.Widget} The widget, for chaining
9062 OO
.ui
.InputWidget
.prototype.setDir = function ( dir
) {
9063 this.$input
.prop( 'dir', dir
);
9068 * Set the value of the input.
9070 * @param {string} value New value
9073 * @return {OO.ui.Widget} The widget, for chaining
9075 OO
.ui
.InputWidget
.prototype.setValue = function ( value
) {
9076 value
= this.cleanUpValue( value
);
9077 // Update the DOM if it has changed. Note that with cleanUpValue, it
9078 // is possible for the DOM value to change without this.value changing.
9079 if ( this.$input
.val() !== value
) {
9080 this.$input
.val( value
);
9082 if ( this.value
!== value
) {
9084 this.emit( 'change', this.value
);
9086 // The first time that the value is set (probably while constructing the widget),
9087 // remember it in defaultValue. This property can be later used to check whether
9088 // the value of the input has been changed since it was created.
9089 if ( this.defaultValue
=== undefined ) {
9090 this.defaultValue
= this.value
;
9091 this.$input
[ 0 ].defaultValue
= this.defaultValue
;
9097 * Clean up incoming value.
9099 * Ensures value is a string, and converts undefined and null to empty string.
9102 * @param {string} value Original value
9103 * @return {string} Cleaned up value
9105 OO
.ui
.InputWidget
.prototype.cleanUpValue = function ( value
) {
9106 if ( value
=== undefined || value
=== null ) {
9108 } else if ( this.inputFilter
) {
9109 return this.inputFilter( String( value
) );
9111 return String( value
);
9118 OO
.ui
.InputWidget
.prototype.setDisabled = function ( state
) {
9119 OO
.ui
.InputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9120 if ( this.$input
) {
9121 this.$input
.prop( 'disabled', this.isDisabled() );
9127 * Set the 'id' attribute of the `<input>` element.
9129 * @param {string} id
9131 * @return {OO.ui.Widget} The widget, for chaining
9133 OO
.ui
.InputWidget
.prototype.setInputId = function ( id
) {
9134 this.$input
.attr( 'id', id
);
9141 OO
.ui
.InputWidget
.prototype.restorePreInfuseState = function ( state
) {
9142 OO
.ui
.InputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9143 if ( state
.value
!== undefined && state
.value
!== this.getValue() ) {
9144 this.setValue( state
.value
);
9146 if ( state
.focus
) {
9152 * Data widget intended for creating `<input type="hidden">` inputs.
9155 * @extends OO.ui.Widget
9158 * @param {Object} [config] Configuration options
9159 * @cfg {string} [value=''] The value of the input.
9160 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
9162 OO
.ui
.HiddenInputWidget
= function OoUiHiddenInputWidget( config
) {
9163 // Configuration initialization
9164 config
= $.extend( { value
: '', name
: '' }, config
);
9166 // Parent constructor
9167 OO
.ui
.HiddenInputWidget
.parent
.call( this, config
);
9170 this.$element
.attr( {
9172 value
: config
.value
,
9175 this.$element
.removeAttr( 'aria-disabled' );
9180 OO
.inheritClass( OO
.ui
.HiddenInputWidget
, OO
.ui
.Widget
);
9182 /* Static Properties */
9188 OO
.ui
.HiddenInputWidget
.static.tagName
= 'input';
9191 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
9192 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
9193 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
9194 * HTML `<button>` (the default) or an HTML `<input>` tags. See the
9195 * [OOUI documentation on MediaWiki] [1] for more information.
9198 * // A ButtonInputWidget rendered as an HTML button, the default.
9199 * var button = new OO.ui.ButtonInputWidget( {
9200 * label: 'Input button',
9204 * $( document.body ).append( button.$element );
9206 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
9209 * @extends OO.ui.InputWidget
9210 * @mixins OO.ui.mixin.ButtonElement
9211 * @mixins OO.ui.mixin.IconElement
9212 * @mixins OO.ui.mixin.IndicatorElement
9213 * @mixins OO.ui.mixin.LabelElement
9216 * @param {Object} [config] Configuration options
9217 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
9218 * @cfg {boolean} [useInputTag=false] Use an `<input>` tag instead of a `<button>` tag, the default.
9219 * Widgets configured to be an `<input>` do not support {@link #icon icons} and {@link #indicator indicators},
9220 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
9221 * be set to `true` when there’s need to support IE 6 in a form with multiple buttons.
9223 OO
.ui
.ButtonInputWidget
= function OoUiButtonInputWidget( config
) {
9224 // Configuration initialization
9225 config
= $.extend( { type
: 'button', useInputTag
: false }, config
);
9227 // See InputWidget#reusePreInfuseDOM about config.$input
9228 if ( config
.$input
) {
9229 config
.$input
.empty();
9232 // Properties (must be set before parent constructor, which calls #setValue)
9233 this.useInputTag
= config
.useInputTag
;
9235 // Parent constructor
9236 OO
.ui
.ButtonInputWidget
.parent
.call( this, config
);
9238 // Mixin constructors
9239 OO
.ui
.mixin
.ButtonElement
.call( this, $.extend( {}, config
, { $button
: this.$input
} ) );
9240 OO
.ui
.mixin
.IconElement
.call( this, config
);
9241 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
9242 OO
.ui
.mixin
.LabelElement
.call( this, config
);
9245 if ( !config
.useInputTag
) {
9246 this.$input
.append( this.$icon
, this.$label
, this.$indicator
);
9248 this.$element
.addClass( 'oo-ui-buttonInputWidget' );
9253 OO
.inheritClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.InputWidget
);
9254 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.ButtonElement
);
9255 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IconElement
);
9256 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
9257 OO
.mixinClass( OO
.ui
.ButtonInputWidget
, OO
.ui
.mixin
.LabelElement
);
9259 /* Static Properties */
9265 OO
.ui
.ButtonInputWidget
.static.tagName
= 'span';
9273 OO
.ui
.ButtonInputWidget
.prototype.getInputElement = function ( config
) {
9275 type
= [ 'button', 'submit', 'reset' ].indexOf( config
.type
) !== -1 ? config
.type
: 'button';
9276 return $( '<' + ( config
.useInputTag
? 'input' : 'button' ) + ' type="' + type
+ '">' );
9282 * If #useInputTag is `true`, the label is set as the `value` of the `<input>` tag.
9284 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
9285 * text, or `null` for no label
9287 * @return {OO.ui.Widget} The widget, for chaining
9289 OO
.ui
.ButtonInputWidget
.prototype.setLabel = function ( label
) {
9290 if ( typeof label
=== 'function' ) {
9291 label
= OO
.ui
.resolveMsg( label
);
9294 if ( this.useInputTag
) {
9295 // Discard non-plaintext labels
9296 if ( typeof label
!== 'string' ) {
9300 this.$input
.val( label
);
9303 return OO
.ui
.mixin
.LabelElement
.prototype.setLabel
.call( this, label
);
9307 * Set the value of the input.
9309 * This method is disabled for button inputs configured as {@link #useInputTag <input> tags}, as
9310 * they do not support {@link #value values}.
9312 * @param {string} value New value
9314 * @return {OO.ui.Widget} The widget, for chaining
9316 OO
.ui
.ButtonInputWidget
.prototype.setValue = function ( value
) {
9317 if ( !this.useInputTag
) {
9318 OO
.ui
.ButtonInputWidget
.parent
.prototype.setValue
.call( this, value
);
9326 OO
.ui
.ButtonInputWidget
.prototype.getInputId = function () {
9327 // Disable generating `<label>` elements for buttons. One would very rarely need additional label
9328 // for a button, and it's already a big clickable target, and it causes unexpected rendering.
9333 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
9334 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
9335 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
9336 * alignment. For more information, please see the [OOUI documentation on MediaWiki][1].
9338 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9341 * // An example of selected, unselected, and disabled checkbox inputs.
9342 * var checkbox1 = new OO.ui.CheckboxInputWidget( {
9346 * checkbox2 = new OO.ui.CheckboxInputWidget( {
9349 * checkbox3 = new OO.ui.CheckboxInputWidget( {
9353 * // Create a fieldset layout with fields for each checkbox.
9354 * fieldset = new OO.ui.FieldsetLayout( {
9355 * label: 'Checkboxes'
9357 * fieldset.addItems( [
9358 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
9359 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
9360 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
9362 * $( document.body ).append( fieldset.$element );
9364 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9367 * @extends OO.ui.InputWidget
9370 * @param {Object} [config] Configuration options
9371 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
9373 OO
.ui
.CheckboxInputWidget
= function OoUiCheckboxInputWidget( config
) {
9374 // Configuration initialization
9375 config
= config
|| {};
9377 // Parent constructor
9378 OO
.ui
.CheckboxInputWidget
.parent
.call( this, config
);
9381 this.checkIcon
= new OO
.ui
.IconWidget( {
9383 classes
: [ 'oo-ui-checkboxInputWidget-checkIcon' ]
9388 .addClass( 'oo-ui-checkboxInputWidget' )
9389 // Required for pretty styling in WikimediaUI theme
9390 .append( this.checkIcon
.$element
);
9391 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9396 OO
.inheritClass( OO
.ui
.CheckboxInputWidget
, OO
.ui
.InputWidget
);
9398 /* Static Properties */
9404 OO
.ui
.CheckboxInputWidget
.static.tagName
= 'span';
9406 /* Static Methods */
9411 OO
.ui
.CheckboxInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9412 var state
= OO
.ui
.CheckboxInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9413 state
.checked
= config
.$input
.prop( 'checked' );
9423 OO
.ui
.CheckboxInputWidget
.prototype.getInputElement = function () {
9424 return $( '<input>' ).attr( 'type', 'checkbox' );
9430 OO
.ui
.CheckboxInputWidget
.prototype.onEdit = function () {
9432 if ( !this.isDisabled() ) {
9433 // Allow the stack to clear so the value will be updated
9434 setTimeout( function () {
9435 widget
.setSelected( widget
.$input
.prop( 'checked' ) );
9441 * Set selection state of this checkbox.
9443 * @param {boolean} state `true` for selected
9445 * @return {OO.ui.Widget} The widget, for chaining
9447 OO
.ui
.CheckboxInputWidget
.prototype.setSelected = function ( state
) {
9449 if ( this.selected
!== state
) {
9450 this.selected
= state
;
9451 this.$input
.prop( 'checked', this.selected
);
9452 this.emit( 'change', this.selected
);
9454 // The first time that the selection state is set (probably while constructing the widget),
9455 // remember it in defaultSelected. This property can be later used to check whether
9456 // the selection state of the input has been changed since it was created.
9457 if ( this.defaultSelected
=== undefined ) {
9458 this.defaultSelected
= this.selected
;
9459 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9465 * Check if this checkbox is selected.
9467 * @return {boolean} Checkbox is selected
9469 OO
.ui
.CheckboxInputWidget
.prototype.isSelected = function () {
9470 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
9471 // it, and we won't know unless they're kind enough to trigger a 'change' event.
9472 var selected
= this.$input
.prop( 'checked' );
9473 if ( this.selected
!== selected
) {
9474 this.setSelected( selected
);
9476 return this.selected
;
9482 OO
.ui
.CheckboxInputWidget
.prototype.simulateLabelClick = function () {
9483 if ( !this.isDisabled() ) {
9484 this.$input
.click();
9492 OO
.ui
.CheckboxInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9493 OO
.ui
.CheckboxInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9494 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9495 this.setSelected( state
.checked
);
9500 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
9501 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9502 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9503 * more information about input widgets.
9505 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
9506 * are no options. If no `value` configuration option is provided, the first option is selected.
9507 * If you need a state representing no value (no option being selected), use a DropdownWidget.
9509 * This and OO.ui.RadioSelectInputWidget support similar configuration options.
9512 * // A DropdownInputWidget with three options.
9513 * var dropdownInput = new OO.ui.DropdownInputWidget( {
9515 * { data: 'a', label: 'First' },
9516 * { data: 'b', label: 'Second', disabled: true },
9517 * { optgroup: 'Group label' },
9518 * { data: 'c', label: 'First sub-item)' }
9521 * $( document.body ).append( dropdownInput.$element );
9523 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9526 * @extends OO.ui.InputWidget
9529 * @param {Object} [config] Configuration options
9530 * @cfg {Object[]} [options=[]] Array of menu options in the format described above.
9531 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
9532 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
9533 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
9534 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
9535 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
9537 OO
.ui
.DropdownInputWidget
= function OoUiDropdownInputWidget( config
) {
9538 // Configuration initialization
9539 config
= config
|| {};
9541 // Properties (must be done before parent constructor which calls #setDisabled)
9542 this.dropdownWidget
= new OO
.ui
.DropdownWidget( $.extend(
9544 $overlay
: config
.$overlay
9548 // Set up the options before parent constructor, which uses them to validate config.value.
9549 // Use this instead of setOptions() because this.$input is not set up yet.
9550 this.setOptionsData( config
.options
|| [] );
9552 // Parent constructor
9553 OO
.ui
.DropdownInputWidget
.parent
.call( this, config
);
9556 this.dropdownWidget
.getMenu().connect( this, { select
: 'onMenuSelect' } );
9560 .addClass( 'oo-ui-dropdownInputWidget' )
9561 .append( this.dropdownWidget
.$element
);
9562 this.setTabIndexedElement( this.dropdownWidget
.$tabIndexed
);
9563 this.setTitledElement( this.dropdownWidget
.$handle
);
9568 OO
.inheritClass( OO
.ui
.DropdownInputWidget
, OO
.ui
.InputWidget
);
9576 OO
.ui
.DropdownInputWidget
.prototype.getInputElement = function () {
9577 return $( '<select>' );
9581 * Handles menu select events.
9584 * @param {OO.ui.MenuOptionWidget|null} item Selected menu item
9586 OO
.ui
.DropdownInputWidget
.prototype.onMenuSelect = function ( item
) {
9587 this.setValue( item
? item
.getData() : '' );
9593 OO
.ui
.DropdownInputWidget
.prototype.setValue = function ( value
) {
9595 value
= this.cleanUpValue( value
);
9596 // Only allow setting values that are actually present in the dropdown
9597 selected
= this.dropdownWidget
.getMenu().findItemFromData( value
) ||
9598 this.dropdownWidget
.getMenu().findFirstSelectableItem();
9599 this.dropdownWidget
.getMenu().selectItem( selected
);
9600 value
= selected
? selected
.getData() : '';
9601 OO
.ui
.DropdownInputWidget
.parent
.prototype.setValue
.call( this, value
);
9602 if ( this.optionsDirty
) {
9603 // We reached this from the constructor or from #setOptions.
9604 // We have to update the <select> element.
9605 this.updateOptionsInterface();
9613 OO
.ui
.DropdownInputWidget
.prototype.setDisabled = function ( state
) {
9614 this.dropdownWidget
.setDisabled( state
);
9615 OO
.ui
.DropdownInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
9620 * Set the options available for this input.
9622 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
9624 * @return {OO.ui.Widget} The widget, for chaining
9626 OO
.ui
.DropdownInputWidget
.prototype.setOptions = function ( options
) {
9627 var value
= this.getValue();
9629 this.setOptionsData( options
);
9631 // Re-set the value to update the visible interface (DropdownWidget and <select>).
9632 // In case the previous value is no longer an available option, select the first valid one.
9633 this.setValue( value
);
9639 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
9641 * This method may be called before the parent constructor, so various properties may not be
9644 * @param {Object[]} options Array of menu options (see #constructor for details).
9647 OO
.ui
.DropdownInputWidget
.prototype.setOptionsData = function ( options
) {
9648 var optionWidgets
, optIndex
, opt
, previousOptgroup
, optionWidget
, optValue
,
9651 this.optionsDirty
= true;
9653 // Go through all the supplied option configs and create either
9654 // MenuSectionOption or MenuOption widgets from each.
9656 for ( optIndex
= 0; optIndex
< options
.length
; optIndex
++ ) {
9657 opt
= options
[ optIndex
];
9659 if ( opt
.optgroup
!== undefined ) {
9660 // Create a <optgroup> menu item.
9661 optionWidget
= widget
.createMenuSectionOptionWidget( opt
.optgroup
);
9662 previousOptgroup
= optionWidget
;
9665 // Create a normal <option> menu item.
9666 optValue
= widget
.cleanUpValue( opt
.data
);
9667 optionWidget
= widget
.createMenuOptionWidget(
9669 opt
.label
!== undefined ? opt
.label
: optValue
9673 // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
9674 if ( opt
.disabled
!== undefined ||
9675 previousOptgroup
instanceof OO
.ui
.MenuSectionOptionWidget
&& previousOptgroup
.isDisabled() ) {
9676 optionWidget
.setDisabled( true );
9679 optionWidgets
.push( optionWidget
);
9682 this.dropdownWidget
.getMenu().clearItems().addItems( optionWidgets
);
9686 * Create a menu option widget.
9689 * @param {string} data Item data
9690 * @param {string} label Item label
9691 * @return {OO.ui.MenuOptionWidget} Option widget
9693 OO
.ui
.DropdownInputWidget
.prototype.createMenuOptionWidget = function ( data
, label
) {
9694 return new OO
.ui
.MenuOptionWidget( {
9701 * Create a menu section option widget.
9704 * @param {string} label Section item label
9705 * @return {OO.ui.MenuSectionOptionWidget} Menu section option widget
9707 OO
.ui
.DropdownInputWidget
.prototype.createMenuSectionOptionWidget = function ( label
) {
9708 return new OO
.ui
.MenuSectionOptionWidget( {
9714 * Update the user-visible interface to match the internal list of options and value.
9716 * This method must only be called after the parent constructor.
9720 OO
.ui
.DropdownInputWidget
.prototype.updateOptionsInterface = function () {
9722 $optionsContainer
= this.$input
,
9723 defaultValue
= this.defaultValue
,
9726 this.$input
.empty();
9728 this.dropdownWidget
.getMenu().getItems().forEach( function ( optionWidget
) {
9731 if ( !( optionWidget
instanceof OO
.ui
.MenuSectionOptionWidget
) ) {
9732 $optionNode
= $( '<option>' )
9733 .attr( 'value', optionWidget
.getData() )
9734 .text( optionWidget
.getLabel() );
9736 // Remember original selection state. This property can be later used to check whether
9737 // the selection state of the input has been changed since it was created.
9738 $optionNode
[ 0 ].defaultSelected
= ( optionWidget
.getData() === defaultValue
);
9740 $optionsContainer
.append( $optionNode
);
9742 $optionNode
= $( '<optgroup>' )
9743 .attr( 'label', optionWidget
.getLabel() );
9744 widget
.$input
.append( $optionNode
);
9745 $optionsContainer
= $optionNode
;
9748 // Disable the option or optgroup if required.
9749 if ( optionWidget
.isDisabled() ) {
9750 $optionNode
.prop( 'disabled', true );
9754 this.optionsDirty
= false;
9760 OO
.ui
.DropdownInputWidget
.prototype.focus = function () {
9761 this.dropdownWidget
.focus();
9768 OO
.ui
.DropdownInputWidget
.prototype.blur = function () {
9769 this.dropdownWidget
.blur();
9774 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
9775 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
9776 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
9777 * please see the [OOUI documentation on MediaWiki][1].
9779 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
9782 * // An example of selected, unselected, and disabled radio inputs
9783 * var radio1 = new OO.ui.RadioInputWidget( {
9787 * var radio2 = new OO.ui.RadioInputWidget( {
9790 * var radio3 = new OO.ui.RadioInputWidget( {
9794 * // Create a fieldset layout with fields for each radio button.
9795 * var fieldset = new OO.ui.FieldsetLayout( {
9796 * label: 'Radio inputs'
9798 * fieldset.addItems( [
9799 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
9800 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
9801 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
9803 * $( document.body ).append( fieldset.$element );
9805 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9808 * @extends OO.ui.InputWidget
9811 * @param {Object} [config] Configuration options
9812 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
9814 OO
.ui
.RadioInputWidget
= function OoUiRadioInputWidget( config
) {
9815 // Configuration initialization
9816 config
= config
|| {};
9818 // Parent constructor
9819 OO
.ui
.RadioInputWidget
.parent
.call( this, config
);
9823 .addClass( 'oo-ui-radioInputWidget' )
9824 // Required for pretty styling in WikimediaUI theme
9825 .append( $( '<span>' ) );
9826 this.setSelected( config
.selected
!== undefined ? config
.selected
: false );
9831 OO
.inheritClass( OO
.ui
.RadioInputWidget
, OO
.ui
.InputWidget
);
9833 /* Static Properties */
9839 OO
.ui
.RadioInputWidget
.static.tagName
= 'span';
9841 /* Static Methods */
9846 OO
.ui
.RadioInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9847 var state
= OO
.ui
.RadioInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9848 state
.checked
= config
.$input
.prop( 'checked' );
9858 OO
.ui
.RadioInputWidget
.prototype.getInputElement = function () {
9859 return $( '<input>' ).attr( 'type', 'radio' );
9865 OO
.ui
.RadioInputWidget
.prototype.onEdit = function () {
9866 // RadioInputWidget doesn't track its state.
9870 * Set selection state of this radio button.
9872 * @param {boolean} state `true` for selected
9874 * @return {OO.ui.Widget} The widget, for chaining
9876 OO
.ui
.RadioInputWidget
.prototype.setSelected = function ( state
) {
9877 // RadioInputWidget doesn't track its state.
9878 this.$input
.prop( 'checked', state
);
9879 // The first time that the selection state is set (probably while constructing the widget),
9880 // remember it in defaultSelected. This property can be later used to check whether
9881 // the selection state of the input has been changed since it was created.
9882 if ( this.defaultSelected
=== undefined ) {
9883 this.defaultSelected
= state
;
9884 this.$input
[ 0 ].defaultChecked
= this.defaultSelected
;
9890 * Check if this radio button is selected.
9892 * @return {boolean} Radio is selected
9894 OO
.ui
.RadioInputWidget
.prototype.isSelected = function () {
9895 return this.$input
.prop( 'checked' );
9901 OO
.ui
.RadioInputWidget
.prototype.simulateLabelClick = function () {
9902 if ( !this.isDisabled() ) {
9903 this.$input
.click();
9911 OO
.ui
.RadioInputWidget
.prototype.restorePreInfuseState = function ( state
) {
9912 OO
.ui
.RadioInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
9913 if ( state
.checked
!== undefined && state
.checked
!== this.isSelected() ) {
9914 this.setSelected( state
.checked
);
9919 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
9920 * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
9921 * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
9922 * more information about input widgets.
9924 * This and OO.ui.DropdownInputWidget support similar configuration options.
9927 * // A RadioSelectInputWidget with three options
9928 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
9930 * { data: 'a', label: 'First' },
9931 * { data: 'b', label: 'Second'},
9932 * { data: 'c', label: 'Third' }
9935 * $( document.body ).append( radioSelectInput.$element );
9937 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
9940 * @extends OO.ui.InputWidget
9943 * @param {Object} [config] Configuration options
9944 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
9946 OO
.ui
.RadioSelectInputWidget
= function OoUiRadioSelectInputWidget( config
) {
9947 // Configuration initialization
9948 config
= config
|| {};
9950 // Properties (must be done before parent constructor which calls #setDisabled)
9951 this.radioSelectWidget
= new OO
.ui
.RadioSelectWidget();
9952 // Set up the options before parent constructor, which uses them to validate config.value.
9953 // Use this instead of setOptions() because this.$input is not set up yet
9954 this.setOptionsData( config
.options
|| [] );
9956 // Parent constructor
9957 OO
.ui
.RadioSelectInputWidget
.parent
.call( this, config
);
9960 this.radioSelectWidget
.connect( this, { select
: 'onMenuSelect' } );
9964 .addClass( 'oo-ui-radioSelectInputWidget' )
9965 .append( this.radioSelectWidget
.$element
);
9966 this.setTabIndexedElement( this.radioSelectWidget
.$tabIndexed
);
9971 OO
.inheritClass( OO
.ui
.RadioSelectInputWidget
, OO
.ui
.InputWidget
);
9973 /* Static Methods */
9978 OO
.ui
.RadioSelectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
9979 var state
= OO
.ui
.RadioSelectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
9980 state
.value
= $( node
).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
9987 OO
.ui
.RadioSelectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
9988 config
= OO
.ui
.RadioSelectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
9989 // Cannot reuse the `<input type=radio>` set
9990 delete config
.$input
;
10000 OO
.ui
.RadioSelectInputWidget
.prototype.getInputElement = function () {
10001 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
10002 // 'value' and 'defaultValue' properties, and InputWidget wants to handle 'defaultValue'.
10003 return $( '<input>' ).addClass( 'oo-ui-element-hidden' );
10007 * Handles menu select events.
10010 * @param {OO.ui.RadioOptionWidget} item Selected menu item
10012 OO
.ui
.RadioSelectInputWidget
.prototype.onMenuSelect = function ( item
) {
10013 this.setValue( item
.getData() );
10019 OO
.ui
.RadioSelectInputWidget
.prototype.setValue = function ( value
) {
10021 value
= this.cleanUpValue( value
);
10022 // Only allow setting values that are actually present in the dropdown
10023 selected
= this.radioSelectWidget
.findItemFromData( value
) ||
10024 this.radioSelectWidget
.findFirstSelectableItem();
10025 this.radioSelectWidget
.selectItem( selected
);
10026 value
= selected
? selected
.getData() : '';
10027 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10034 OO
.ui
.RadioSelectInputWidget
.prototype.setDisabled = function ( state
) {
10035 this.radioSelectWidget
.setDisabled( state
);
10036 OO
.ui
.RadioSelectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10041 * Set the options available for this input.
10043 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10045 * @return {OO.ui.Widget} The widget, for chaining
10047 OO
.ui
.RadioSelectInputWidget
.prototype.setOptions = function ( options
) {
10048 var value
= this.getValue();
10050 this.setOptionsData( options
);
10052 // Re-set the value to update the visible interface (RadioSelectWidget).
10053 // In case the previous value is no longer an available option, select the first valid one.
10054 this.setValue( value
);
10060 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10062 * This method may be called before the parent constructor, so various properties may not be
10065 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10068 OO
.ui
.RadioSelectInputWidget
.prototype.setOptionsData = function ( options
) {
10071 this.radioSelectWidget
10073 .addItems( options
.map( function ( opt
) {
10074 var optValue
= widget
.cleanUpValue( opt
.data
);
10075 return new OO
.ui
.RadioOptionWidget( {
10077 label
: opt
.label
!== undefined ? opt
.label
: optValue
10085 OO
.ui
.RadioSelectInputWidget
.prototype.focus = function () {
10086 this.radioSelectWidget
.focus();
10093 OO
.ui
.RadioSelectInputWidget
.prototype.blur = function () {
10094 this.radioSelectWidget
.blur();
10099 * CheckboxMultiselectInputWidget is a
10100 * {@link OO.ui.CheckboxMultiselectWidget CheckboxMultiselectWidget} intended to be used within a
10101 * HTML form, such as a OO.ui.FormLayout. The selected values are synchronized with the value of
10102 * HTML `<input type=checkbox>` tags. Please see the [OOUI documentation on MediaWiki][1] for
10103 * more information about input widgets.
10106 * // A CheckboxMultiselectInputWidget with three options.
10107 * var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
10109 * { data: 'a', label: 'First' },
10110 * { data: 'b', label: 'Second' },
10111 * { data: 'c', label: 'Third' }
10114 * $( document.body ).append( multiselectInput.$element );
10116 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10119 * @extends OO.ui.InputWidget
10122 * @param {Object} [config] Configuration options
10123 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
10125 OO
.ui
.CheckboxMultiselectInputWidget
= function OoUiCheckboxMultiselectInputWidget( config
) {
10126 // Configuration initialization
10127 config
= config
|| {};
10129 // Properties (must be done before parent constructor which calls #setDisabled)
10130 this.checkboxMultiselectWidget
= new OO
.ui
.CheckboxMultiselectWidget();
10131 // Must be set before the #setOptionsData call below
10132 this.inputName
= config
.name
;
10133 // Set up the options before parent constructor, which uses them to validate config.value.
10134 // Use this instead of setOptions() because this.$input is not set up yet
10135 this.setOptionsData( config
.options
|| [] );
10137 // Parent constructor
10138 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.call( this, config
);
10141 this.checkboxMultiselectWidget
.connect( this, { select
: 'onCheckboxesSelect' } );
10145 .addClass( 'oo-ui-checkboxMultiselectInputWidget' )
10146 .append( this.checkboxMultiselectWidget
.$element
);
10147 // We don't use this.$input, but rather the CheckboxInputWidgets inside each option
10148 this.$input
.detach();
10153 OO
.inheritClass( OO
.ui
.CheckboxMultiselectInputWidget
, OO
.ui
.InputWidget
);
10155 /* Static Methods */
10160 OO
.ui
.CheckboxMultiselectInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
10161 var state
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
10162 state
.value
= $( node
).find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10163 .toArray().map( function ( el
) { return el
.value
; } );
10170 OO
.ui
.CheckboxMultiselectInputWidget
.static.reusePreInfuseDOM = function ( node
, config
) {
10171 config
= OO
.ui
.CheckboxMultiselectInputWidget
.parent
.static.reusePreInfuseDOM( node
, config
);
10172 // Cannot reuse the `<input type=checkbox>` set
10173 delete config
.$input
;
10183 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getInputElement = function () {
10185 return $( '<unused>' );
10189 * Handles CheckboxMultiselectWidget select events.
10193 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.onCheckboxesSelect = function () {
10194 this.setValue( this.checkboxMultiselectWidget
.findSelectedItemsData() );
10200 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.getValue = function () {
10201 var value
= this.$element
.find( '.oo-ui-checkboxInputWidget .oo-ui-inputWidget-input:checked' )
10202 .toArray().map( function ( el
) { return el
.value
; } );
10203 if ( this.value
!== value
) {
10204 this.setValue( value
);
10212 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setValue = function ( value
) {
10213 value
= this.cleanUpValue( value
);
10214 this.checkboxMultiselectWidget
.selectItemsByData( value
);
10215 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setValue
.call( this, value
);
10216 if ( this.optionsDirty
) {
10217 // We reached this from the constructor or from #setOptions.
10218 // We have to update the <select> element.
10219 this.updateOptionsInterface();
10225 * Clean up incoming value.
10227 * @param {string[]} value Original value
10228 * @return {string[]} Cleaned up value
10230 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.cleanUpValue = function ( value
) {
10231 var i
, singleValue
,
10233 if ( !Array
.isArray( value
) ) {
10236 for ( i
= 0; i
< value
.length
; i
++ ) {
10238 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
[ i
] );
10239 // Remove options that we don't have here
10240 if ( !this.checkboxMultiselectWidget
.findItemFromData( singleValue
) ) {
10243 cleanValue
.push( singleValue
);
10251 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setDisabled = function ( state
) {
10252 this.checkboxMultiselectWidget
.setDisabled( state
);
10253 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.setDisabled
.call( this, state
);
10258 * Set the options available for this input.
10260 * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
10262 * @return {OO.ui.Widget} The widget, for chaining
10264 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptions = function ( options
) {
10265 var value
= this.getValue();
10267 this.setOptionsData( options
);
10269 // Re-set the value to update the visible interface (CheckboxMultiselectWidget).
10270 // This will also get rid of any stale options that we just removed.
10271 this.setValue( value
);
10277 * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
10279 * This method may be called before the parent constructor, so various properties may not be
10282 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10285 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.setOptionsData = function ( options
) {
10288 this.optionsDirty
= true;
10290 this.checkboxMultiselectWidget
10292 .addItems( options
.map( function ( opt
) {
10293 var optValue
, item
, optDisabled
;
10295 OO
.ui
.CheckboxMultiselectInputWidget
.parent
.prototype.cleanUpValue
.call( widget
, opt
.data
);
10296 optDisabled
= opt
.disabled
!== undefined ? opt
.disabled
: false;
10297 item
= new OO
.ui
.CheckboxMultioptionWidget( {
10299 label
: opt
.label
!== undefined ? opt
.label
: optValue
,
10300 disabled
: optDisabled
10302 // Set the 'name' and 'value' for form submission
10303 item
.checkbox
.$input
.attr( 'name', widget
.inputName
);
10304 item
.checkbox
.setValue( optValue
);
10310 * Update the user-visible interface to match the internal list of options and value.
10312 * This method must only be called after the parent constructor.
10316 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.updateOptionsInterface = function () {
10317 var defaultValue
= this.defaultValue
;
10319 this.checkboxMultiselectWidget
.getItems().forEach( function ( item
) {
10320 // Remember original selection state. This property can be later used to check whether
10321 // the selection state of the input has been changed since it was created.
10322 var isDefault
= defaultValue
.indexOf( item
.getData() ) !== -1;
10323 item
.checkbox
.defaultSelected
= isDefault
;
10324 item
.checkbox
.$input
[ 0 ].defaultChecked
= isDefault
;
10327 this.optionsDirty
= false;
10333 OO
.ui
.CheckboxMultiselectInputWidget
.prototype.focus = function () {
10334 this.checkboxMultiselectWidget
.focus();
10339 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
10340 * size of the field as well as its presentation. In addition, these widgets can be configured
10341 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
10342 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
10343 * which modifies incoming values rather than validating them.
10344 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
10346 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
10349 * // A TextInputWidget.
10350 * var textInput = new OO.ui.TextInputWidget( {
10351 * value: 'Text input'
10353 * $( document.body ).append( textInput.$element );
10355 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
10358 * @extends OO.ui.InputWidget
10359 * @mixins OO.ui.mixin.IconElement
10360 * @mixins OO.ui.mixin.IndicatorElement
10361 * @mixins OO.ui.mixin.PendingElement
10362 * @mixins OO.ui.mixin.LabelElement
10365 * @param {Object} [config] Configuration options
10366 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password'
10367 * 'email', 'url' or 'number'.
10368 * @cfg {string} [placeholder] Placeholder text
10369 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
10370 * instruct the browser to focus this widget.
10371 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
10372 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
10374 * For unfortunate historical reasons, this counts the number of UTF-16 code units rather than
10375 * Unicode codepoints, which means that codepoints outside the Basic Multilingual Plane (e.g.
10376 * many emojis) count as 2 characters each.
10377 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
10378 * the value or placeholder text: `'before'` or `'after'`
10379 * @cfg {boolean} [required=false] Mark the field as required with `true`. Implies `indicator: 'required'`.
10380 * Note that `false` & setting `indicator: 'required' will result in no indicator shown.
10381 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
10382 * @cfg {boolean} [spellcheck] Should the browser support spellcheck for this field (`undefined` means
10383 * leaving it up to the browser).
10384 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
10385 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
10386 * (the value must contain only numbers); when RegExp, a regular expression that must match the
10387 * value for it to be considered valid; when Function, a function receiving the value as parameter
10388 * that must return true, or promise resolving to true, for it to be considered valid.
10390 OO
.ui
.TextInputWidget
= function OoUiTextInputWidget( config
) {
10391 // Configuration initialization
10392 config
= $.extend( {
10394 labelPosition
: 'after'
10397 // Parent constructor
10398 OO
.ui
.TextInputWidget
.parent
.call( this, config
);
10400 // Mixin constructors
10401 OO
.ui
.mixin
.IconElement
.call( this, config
);
10402 OO
.ui
.mixin
.IndicatorElement
.call( this, config
);
10403 OO
.ui
.mixin
.PendingElement
.call( this, $.extend( {}, config
, { $pending
: this.$input
} ) );
10404 OO
.ui
.mixin
.LabelElement
.call( this, config
);
10407 this.type
= this.getSaneType( config
);
10408 this.readOnly
= false;
10409 this.required
= false;
10410 this.validate
= null;
10411 this.scrollWidth
= null;
10413 this.setValidation( config
.validate
);
10414 this.setLabelPosition( config
.labelPosition
);
10418 keypress
: this.onKeyPress
.bind( this ),
10419 blur
: this.onBlur
.bind( this ),
10420 focus
: this.onFocus
.bind( this )
10422 this.$icon
.on( 'mousedown', this.onIconMouseDown
.bind( this ) );
10423 this.$indicator
.on( 'mousedown', this.onIndicatorMouseDown
.bind( this ) );
10424 this.on( 'labelChange', this.updatePosition
.bind( this ) );
10425 this.on( 'change', OO
.ui
.debounce( this.onDebouncedChange
.bind( this ), 250 ) );
10429 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type
)
10430 .append( this.$icon
, this.$indicator
);
10431 this.setReadOnly( !!config
.readOnly
);
10432 this.setRequired( !!config
.required
);
10433 if ( config
.placeholder
!== undefined ) {
10434 this.$input
.attr( 'placeholder', config
.placeholder
);
10436 if ( config
.maxLength
!== undefined ) {
10437 this.$input
.attr( 'maxlength', config
.maxLength
);
10439 if ( config
.autofocus
) {
10440 this.$input
.attr( 'autofocus', 'autofocus' );
10442 if ( config
.autocomplete
=== false ) {
10443 this.$input
.attr( 'autocomplete', 'off' );
10444 // Turning off autocompletion also disables "form caching" when the user navigates to a
10445 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
10447 beforeunload: function () {
10448 this.$input
.removeAttr( 'autocomplete' );
10450 pageshow: function () {
10451 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
10452 // whole page... it shouldn't hurt, though.
10453 this.$input
.attr( 'autocomplete', 'off' );
10457 if ( config
.spellcheck
!== undefined ) {
10458 this.$input
.attr( 'spellcheck', config
.spellcheck
? 'true' : 'false' );
10460 if ( this.label
) {
10461 this.isWaitingToBeAttached
= true;
10462 this.installParentChangeDetector();
10468 OO
.inheritClass( OO
.ui
.TextInputWidget
, OO
.ui
.InputWidget
);
10469 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IconElement
);
10470 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.IndicatorElement
);
10471 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.PendingElement
);
10472 OO
.mixinClass( OO
.ui
.TextInputWidget
, OO
.ui
.mixin
.LabelElement
);
10474 /* Static Properties */
10476 OO
.ui
.TextInputWidget
.static.validationPatterns
= {
10484 * An `enter` event is emitted when the user presses 'enter' inside the text box.
10492 * Handle icon mouse down events.
10495 * @param {jQuery.Event} e Mouse down event
10496 * @return {undefined/boolean} False to prevent default if event is handled
10498 OO
.ui
.TextInputWidget
.prototype.onIconMouseDown = function ( e
) {
10499 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10506 * Handle indicator mouse down events.
10509 * @param {jQuery.Event} e Mouse down event
10510 * @return {undefined/boolean} False to prevent default if event is handled
10512 OO
.ui
.TextInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
10513 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
10520 * Handle key press events.
10523 * @param {jQuery.Event} e Key press event
10524 * @fires enter If enter key is pressed
10526 OO
.ui
.TextInputWidget
.prototype.onKeyPress = function ( e
) {
10527 if ( e
.which
=== OO
.ui
.Keys
.ENTER
) {
10528 this.emit( 'enter', e
);
10533 * Handle blur events.
10536 * @param {jQuery.Event} e Blur event
10538 OO
.ui
.TextInputWidget
.prototype.onBlur = function () {
10539 this.setValidityFlag();
10543 * Handle focus events.
10546 * @param {jQuery.Event} e Focus event
10548 OO
.ui
.TextInputWidget
.prototype.onFocus = function () {
10549 if ( this.isWaitingToBeAttached
) {
10550 // If we've received focus, then we must be attached to the document, and if
10551 // isWaitingToBeAttached is still true, that means the handler never fired. Fire it now.
10552 this.onElementAttach();
10554 this.setValidityFlag( true );
10558 * Handle element attach events.
10561 * @param {jQuery.Event} e Element attach event
10563 OO
.ui
.TextInputWidget
.prototype.onElementAttach = function () {
10564 this.isWaitingToBeAttached
= false;
10565 // Any previously calculated size is now probably invalid if we reattached elsewhere
10566 this.valCache
= null;
10567 this.positionLabel();
10571 * Handle debounced change events.
10573 * @param {string} value
10576 OO
.ui
.TextInputWidget
.prototype.onDebouncedChange = function () {
10577 this.setValidityFlag();
10581 * Check if the input is {@link #readOnly read-only}.
10583 * @return {boolean}
10585 OO
.ui
.TextInputWidget
.prototype.isReadOnly = function () {
10586 return this.readOnly
;
10590 * Set the {@link #readOnly read-only} state of the input.
10592 * @param {boolean} state Make input read-only
10594 * @return {OO.ui.Widget} The widget, for chaining
10596 OO
.ui
.TextInputWidget
.prototype.setReadOnly = function ( state
) {
10597 this.readOnly
= !!state
;
10598 this.$input
.prop( 'readOnly', this.readOnly
);
10603 * Check if the input is {@link #required required}.
10605 * @return {boolean}
10607 OO
.ui
.TextInputWidget
.prototype.isRequired = function () {
10608 return this.required
;
10612 * Set the {@link #required required} state of the input.
10614 * @param {boolean} state Make input required
10616 * @return {OO.ui.Widget} The widget, for chaining
10618 OO
.ui
.TextInputWidget
.prototype.setRequired = function ( state
) {
10619 this.required
= !!state
;
10620 if ( this.required
) {
10622 .prop( 'required', true )
10623 .attr( 'aria-required', 'true' );
10624 if ( this.getIndicator() === null ) {
10625 this.setIndicator( 'required' );
10629 .prop( 'required', false )
10630 .removeAttr( 'aria-required' );
10631 if ( this.getIndicator() === 'required' ) {
10632 this.setIndicator( null );
10639 * Support function for making #onElementAttach work across browsers.
10641 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
10642 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
10644 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
10645 * first time that the element gets attached to the documented.
10647 OO
.ui
.TextInputWidget
.prototype.installParentChangeDetector = function () {
10648 var mutationObserver
, onRemove
, topmostNode
, fakeParentNode
,
10649 MutationObserver
= window
.MutationObserver
|| window
.WebKitMutationObserver
|| window
.MozMutationObserver
,
10652 if ( MutationObserver
) {
10653 // The new way. If only it wasn't so ugly.
10655 if ( this.isElementAttached() ) {
10656 // Widget is attached already, do nothing. This breaks the functionality of this function when
10657 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
10658 // would require observation of the whole document, which would hurt performance of other,
10659 // more important code.
10663 // Find topmost node in the tree
10664 topmostNode
= this.$element
[ 0 ];
10665 while ( topmostNode
.parentNode
) {
10666 topmostNode
= topmostNode
.parentNode
;
10669 // We have no way to detect the $element being attached somewhere without observing the entire
10670 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
10671 // parent node of $element, and instead detect when $element is removed from it (and thus
10672 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
10673 // doesn't get attached, we end up back here and create the parent.
10675 mutationObserver
= new MutationObserver( function ( mutations
) {
10676 var i
, j
, removedNodes
;
10677 for ( i
= 0; i
< mutations
.length
; i
++ ) {
10678 removedNodes
= mutations
[ i
].removedNodes
;
10679 for ( j
= 0; j
< removedNodes
.length
; j
++ ) {
10680 if ( removedNodes
[ j
] === topmostNode
) {
10681 setTimeout( onRemove
, 0 );
10688 onRemove = function () {
10689 // If the node was attached somewhere else, report it
10690 if ( widget
.isElementAttached() ) {
10691 widget
.onElementAttach();
10693 mutationObserver
.disconnect();
10694 widget
.installParentChangeDetector();
10697 // Create a fake parent and observe it
10698 fakeParentNode
= $( '<div>' ).append( topmostNode
)[ 0 ];
10699 mutationObserver
.observe( fakeParentNode
, { childList
: true } );
10701 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
10702 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
10703 this.$element
.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach
.bind( this ) );
10711 OO
.ui
.TextInputWidget
.prototype.getInputElement = function ( config
) {
10712 if ( this.getSaneType( config
) === 'number' ) {
10713 return $( '<input>' )
10714 .attr( 'step', 'any' )
10715 .attr( 'type', 'number' );
10717 return $( '<input>' ).attr( 'type', this.getSaneType( config
) );
10722 * Get sanitized value for 'type' for given config.
10724 * @param {Object} config Configuration options
10725 * @return {string|null}
10728 OO
.ui
.TextInputWidget
.prototype.getSaneType = function ( config
) {
10729 var allowedTypes
= [
10736 return allowedTypes
.indexOf( config
.type
) !== -1 ? config
.type
: 'text';
10740 * Focus the input and select a specified range within the text.
10742 * @param {number} from Select from offset
10743 * @param {number} [to] Select to offset, defaults to from
10745 * @return {OO.ui.Widget} The widget, for chaining
10747 OO
.ui
.TextInputWidget
.prototype.selectRange = function ( from, to
) {
10748 var isBackwards
, start
, end
,
10749 input
= this.$input
[ 0 ];
10753 isBackwards
= to
< from;
10754 start
= isBackwards
? to
: from;
10755 end
= isBackwards
? from : to
;
10760 input
.setSelectionRange( start
, end
, isBackwards
? 'backward' : 'forward' );
10762 // IE throws an exception if you call setSelectionRange on a unattached DOM node.
10763 // Rather than expensively check if the input is attached every time, just check
10764 // if it was the cause of an error being thrown. If not, rethrow the error.
10765 if ( this.getElementDocument().body
.contains( input
) ) {
10773 * Get an object describing the current selection range in a directional manner
10775 * @return {Object} Object containing 'from' and 'to' offsets
10777 OO
.ui
.TextInputWidget
.prototype.getRange = function () {
10778 var input
= this.$input
[ 0 ],
10779 start
= input
.selectionStart
,
10780 end
= input
.selectionEnd
,
10781 isBackwards
= input
.selectionDirection
=== 'backward';
10784 from: isBackwards
? end
: start
,
10785 to
: isBackwards
? start
: end
10790 * Get the length of the text input value.
10792 * This could differ from the length of #getValue if the
10793 * value gets filtered
10795 * @return {number} Input length
10797 OO
.ui
.TextInputWidget
.prototype.getInputLength = function () {
10798 return this.$input
[ 0 ].value
.length
;
10802 * Focus the input and select the entire text.
10805 * @return {OO.ui.Widget} The widget, for chaining
10807 OO
.ui
.TextInputWidget
.prototype.select = function () {
10808 return this.selectRange( 0, this.getInputLength() );
10812 * Focus the input and move the cursor to the start.
10815 * @return {OO.ui.Widget} The widget, for chaining
10817 OO
.ui
.TextInputWidget
.prototype.moveCursorToStart = function () {
10818 return this.selectRange( 0 );
10822 * Focus the input and move the cursor to the end.
10825 * @return {OO.ui.Widget} The widget, for chaining
10827 OO
.ui
.TextInputWidget
.prototype.moveCursorToEnd = function () {
10828 return this.selectRange( this.getInputLength() );
10832 * Insert new content into the input.
10834 * @param {string} content Content to be inserted
10836 * @return {OO.ui.Widget} The widget, for chaining
10838 OO
.ui
.TextInputWidget
.prototype.insertContent = function ( content
) {
10840 range
= this.getRange(),
10841 value
= this.getValue();
10843 start
= Math
.min( range
.from, range
.to
);
10844 end
= Math
.max( range
.from, range
.to
);
10846 this.setValue( value
.slice( 0, start
) + content
+ value
.slice( end
) );
10847 this.selectRange( start
+ content
.length
);
10852 * Insert new content either side of a selection.
10854 * @param {string} pre Content to be inserted before the selection
10855 * @param {string} post Content to be inserted after the selection
10857 * @return {OO.ui.Widget} The widget, for chaining
10859 OO
.ui
.TextInputWidget
.prototype.encapsulateContent = function ( pre
, post
) {
10861 range
= this.getRange(),
10862 offset
= pre
.length
;
10864 start
= Math
.min( range
.from, range
.to
);
10865 end
= Math
.max( range
.from, range
.to
);
10867 this.selectRange( start
).insertContent( pre
);
10868 this.selectRange( offset
+ end
).insertContent( post
);
10870 this.selectRange( offset
+ start
, offset
+ end
);
10875 * Set the validation pattern.
10877 * The validation pattern is either a regular expression, a function, or the symbolic name of a
10878 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
10879 * value must contain only numbers).
10881 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
10882 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
10884 OO
.ui
.TextInputWidget
.prototype.setValidation = function ( validate
) {
10885 if ( validate
instanceof RegExp
|| validate
instanceof Function
) {
10886 this.validate
= validate
;
10888 this.validate
= this.constructor.static.validationPatterns
[ validate
] || /.*/;
10893 * Sets the 'invalid' flag appropriately.
10895 * @param {boolean} [isValid] Optionally override validation result
10897 OO
.ui
.TextInputWidget
.prototype.setValidityFlag = function ( isValid
) {
10899 setFlag = function ( valid
) {
10901 widget
.$input
.attr( 'aria-invalid', 'true' );
10903 widget
.$input
.removeAttr( 'aria-invalid' );
10905 widget
.setFlags( { invalid
: !valid
} );
10908 if ( isValid
!== undefined ) {
10909 setFlag( isValid
);
10911 this.getValidity().then( function () {
10920 * Get the validity of current value.
10922 * This method returns a promise that resolves if the value is valid and rejects if
10923 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
10925 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
10927 OO
.ui
.TextInputWidget
.prototype.getValidity = function () {
10930 function rejectOrResolve( valid
) {
10932 return $.Deferred().resolve().promise();
10934 return $.Deferred().reject().promise();
10938 // Check browser validity and reject if it is invalid
10940 this.$input
[ 0 ].checkValidity
!== undefined &&
10941 this.$input
[ 0 ].checkValidity() === false
10943 return rejectOrResolve( false );
10946 // Run our checks if the browser thinks the field is valid
10947 if ( this.validate
instanceof Function
) {
10948 result
= this.validate( this.getValue() );
10949 if ( result
&& typeof result
.promise
=== 'function' ) {
10950 return result
.promise().then( function ( valid
) {
10951 return rejectOrResolve( valid
);
10954 return rejectOrResolve( result
);
10957 return rejectOrResolve( this.getValue().match( this.validate
) );
10962 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
10964 * @param {string} labelPosition Label position, 'before' or 'after'
10966 * @return {OO.ui.Widget} The widget, for chaining
10968 OO
.ui
.TextInputWidget
.prototype.setLabelPosition = function ( labelPosition
) {
10969 this.labelPosition
= labelPosition
;
10970 if ( this.label
) {
10971 // If there is no label and we only change the position, #updatePosition is a no-op,
10972 // but it takes really a lot of work to do nothing.
10973 this.updatePosition();
10979 * Update the position of the inline label.
10981 * This method is called by #setLabelPosition, and can also be called on its own if
10982 * something causes the label to be mispositioned.
10985 * @return {OO.ui.Widget} The widget, for chaining
10987 OO
.ui
.TextInputWidget
.prototype.updatePosition = function () {
10988 var after
= this.labelPosition
=== 'after';
10991 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label
&& after
)
10992 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label
&& !after
);
10994 this.valCache
= null;
10995 this.scrollWidth
= null;
10996 this.positionLabel();
11002 * Position the label by setting the correct padding on the input.
11006 * @return {OO.ui.Widget} The widget, for chaining
11008 OO
.ui
.TextInputWidget
.prototype.positionLabel = function () {
11009 var after
, rtl
, property
, newCss
;
11011 if ( this.isWaitingToBeAttached
) {
11012 // #onElementAttach will be called soon, which calls this method
11017 'padding-right': '',
11021 if ( this.label
) {
11022 this.$element
.append( this.$label
);
11024 this.$label
.detach();
11025 // Clear old values if present
11026 this.$input
.css( newCss
);
11030 after
= this.labelPosition
=== 'after';
11031 rtl
= this.$element
.css( 'direction' ) === 'rtl';
11032 property
= after
=== rtl
? 'padding-left' : 'padding-right';
11034 newCss
[ property
] = this.$label
.outerWidth( true ) + ( after
? this.scrollWidth
: 0 );
11035 // We have to clear the padding on the other side, in case the element direction changed
11036 this.$input
.css( newCss
);
11042 * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
11043 * {@link OO.ui.mixin.IconElement search icon} by default.
11044 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11046 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
11049 * @extends OO.ui.TextInputWidget
11052 * @param {Object} [config] Configuration options
11054 OO
.ui
.SearchInputWidget
= function OoUiSearchInputWidget( config
) {
11055 config
= $.extend( {
11059 // Parent constructor
11060 OO
.ui
.SearchInputWidget
.parent
.call( this, config
);
11063 this.connect( this, {
11068 this.updateSearchIndicator();
11069 this.connect( this, {
11070 disable
: 'onDisable'
11076 OO
.inheritClass( OO
.ui
.SearchInputWidget
, OO
.ui
.TextInputWidget
);
11084 OO
.ui
.SearchInputWidget
.prototype.getSaneType = function () {
11091 OO
.ui
.SearchInputWidget
.prototype.onIndicatorMouseDown = function ( e
) {
11092 if ( e
.which
=== OO
.ui
.MouseButtons
.LEFT
) {
11093 // Clear the text field
11094 this.setValue( '' );
11101 * Update the 'clear' indicator displayed on type: 'search' text
11102 * fields, hiding it when the field is already empty or when it's not
11105 OO
.ui
.SearchInputWidget
.prototype.updateSearchIndicator = function () {
11106 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
11107 this.setIndicator( null );
11109 this.setIndicator( 'clear' );
11114 * Handle change events.
11118 OO
.ui
.SearchInputWidget
.prototype.onChange = function () {
11119 this.updateSearchIndicator();
11123 * Handle disable events.
11125 * @param {boolean} disabled Element is disabled
11128 OO
.ui
.SearchInputWidget
.prototype.onDisable = function () {
11129 this.updateSearchIndicator();
11135 OO
.ui
.SearchInputWidget
.prototype.setReadOnly = function ( state
) {
11136 OO
.ui
.SearchInputWidget
.parent
.prototype.setReadOnly
.call( this, state
);
11137 this.updateSearchIndicator();
11142 * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
11143 * configure number of rows visible. In addition, these widgets can be autosized to fit user
11144 * inputs and can show {@link OO.ui.mixin.IconElement icons} and
11145 * {@link OO.ui.mixin.IndicatorElement indicators}.
11146 * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
11148 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11151 * // A MultilineTextInputWidget.
11152 * var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
11153 * value: 'Text input on multiple lines'
11155 * $( 'body' ).append( multilineTextInput.$element );
11157 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
11160 * @extends OO.ui.TextInputWidget
11163 * @param {Object} [config] Configuration options
11164 * @cfg {number} [rows] Number of visible lines in textarea. If used with `autosize`,
11165 * specifies minimum number of rows to display.
11166 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11167 * Use the #maxRows config to specify a maximum number of displayed rows.
11168 * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
11169 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
11171 OO
.ui
.MultilineTextInputWidget
= function OoUiMultilineTextInputWidget( config
) {
11172 config
= $.extend( {
11175 // Parent constructor
11176 OO
.ui
.MultilineTextInputWidget
.parent
.call( this, config
);
11179 this.autosize
= !!config
.autosize
;
11180 this.styleHeight
= null;
11181 this.minRows
= config
.rows
!== undefined ? config
.rows
: '';
11182 this.maxRows
= config
.maxRows
|| Math
.max( 2 * ( this.minRows
|| 0 ), 10 );
11184 // Clone for resizing
11185 if ( this.autosize
) {
11186 this.$clone
= this.$input
11188 .removeAttr( 'id' )
11189 .removeAttr( 'name' )
11190 .insertAfter( this.$input
)
11191 .attr( 'aria-hidden', 'true' )
11192 .addClass( 'oo-ui-element-hidden' );
11196 this.connect( this, {
11201 if ( config
.rows
) {
11202 this.$input
.attr( 'rows', config
.rows
);
11204 if ( this.autosize
) {
11205 this.$input
.addClass( 'oo-ui-textInputWidget-autosized' );
11206 this.isWaitingToBeAttached
= true;
11207 this.installParentChangeDetector();
11213 OO
.inheritClass( OO
.ui
.MultilineTextInputWidget
, OO
.ui
.TextInputWidget
);
11215 /* Static Methods */
11220 OO
.ui
.MultilineTextInputWidget
.static.gatherPreInfuseState = function ( node
, config
) {
11221 var state
= OO
.ui
.MultilineTextInputWidget
.parent
.static.gatherPreInfuseState( node
, config
);
11222 state
.scrollTop
= config
.$input
.scrollTop();
11231 OO
.ui
.MultilineTextInputWidget
.prototype.onElementAttach = function () {
11232 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.onElementAttach
.call( this );
11237 * Handle change events.
11241 OO
.ui
.MultilineTextInputWidget
.prototype.onChange = function () {
11248 OO
.ui
.MultilineTextInputWidget
.prototype.updatePosition = function () {
11249 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.updatePosition
.call( this );
11256 * Modify to emit 'enter' on Ctrl/Meta+Enter, instead of plain Enter
11258 OO
.ui
.MultilineTextInputWidget
.prototype.onKeyPress = function ( e
) {
11260 ( e
.which
=== OO
.ui
.Keys
.ENTER
&& ( e
.ctrlKey
|| e
.metaKey
) ) ||
11261 // Some platforms emit keycode 10 for ctrl+enter in a textarea
11264 this.emit( 'enter', e
);
11269 * Automatically adjust the size of the text input.
11271 * This only affects multiline inputs that are {@link #autosize autosized}.
11274 * @return {OO.ui.Widget} The widget, for chaining
11277 OO
.ui
.MultilineTextInputWidget
.prototype.adjustSize = function () {
11278 var scrollHeight
, innerHeight
, outerHeight
, maxInnerHeight
, measurementError
,
11279 idealHeight
, newHeight
, scrollWidth
, property
;
11281 if ( this.$input
.val() !== this.valCache
) {
11282 if ( this.autosize
) {
11284 .val( this.$input
.val() )
11285 .attr( 'rows', this.minRows
)
11286 // Set inline height property to 0 to measure scroll height
11287 .css( 'height', 0 );
11289 this.$clone
.removeClass( 'oo-ui-element-hidden' );
11291 this.valCache
= this.$input
.val();
11293 scrollHeight
= this.$clone
[ 0 ].scrollHeight
;
11295 // Remove inline height property to measure natural heights
11296 this.$clone
.css( 'height', '' );
11297 innerHeight
= this.$clone
.innerHeight();
11298 outerHeight
= this.$clone
.outerHeight();
11300 // Measure max rows height
11302 .attr( 'rows', this.maxRows
)
11303 .css( 'height', 'auto' )
11305 maxInnerHeight
= this.$clone
.innerHeight();
11307 // Difference between reported innerHeight and scrollHeight with no scrollbars present.
11308 // This is sometimes non-zero on Blink-based browsers, depending on zoom level.
11309 measurementError
= maxInnerHeight
- this.$clone
[ 0 ].scrollHeight
;
11310 idealHeight
= Math
.min( maxInnerHeight
, scrollHeight
+ measurementError
);
11312 this.$clone
.addClass( 'oo-ui-element-hidden' );
11314 // Only apply inline height when expansion beyond natural height is needed
11315 // Use the difference between the inner and outer height as a buffer
11316 newHeight
= idealHeight
> innerHeight
? idealHeight
+ ( outerHeight
- innerHeight
) : '';
11317 if ( newHeight
!== this.styleHeight
) {
11318 this.$input
.css( 'height', newHeight
);
11319 this.styleHeight
= newHeight
;
11320 this.emit( 'resize' );
11323 scrollWidth
= this.$input
[ 0 ].offsetWidth
- this.$input
[ 0 ].clientWidth
;
11324 if ( scrollWidth
!== this.scrollWidth
) {
11325 property
= this.$element
.css( 'direction' ) === 'rtl' ? 'left' : 'right';
11327 this.$label
.css( { right
: '', left
: '' } );
11328 this.$indicator
.css( { right
: '', left
: '' } );
11330 if ( scrollWidth
) {
11331 this.$indicator
.css( property
, scrollWidth
);
11332 if ( this.labelPosition
=== 'after' ) {
11333 this.$label
.css( property
, scrollWidth
);
11337 this.scrollWidth
= scrollWidth
;
11338 this.positionLabel();
11348 OO
.ui
.MultilineTextInputWidget
.prototype.getInputElement = function () {
11349 return $( '<textarea>' );
11353 * Check if the input automatically adjusts its size.
11355 * @return {boolean}
11357 OO
.ui
.MultilineTextInputWidget
.prototype.isAutosizing = function () {
11358 return !!this.autosize
;
11364 OO
.ui
.MultilineTextInputWidget
.prototype.restorePreInfuseState = function ( state
) {
11365 OO
.ui
.MultilineTextInputWidget
.parent
.prototype.restorePreInfuseState
.call( this, state
);
11366 if ( state
.scrollTop
!== undefined ) {
11367 this.$input
.scrollTop( state
.scrollTop
);
11372 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11373 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11374 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11376 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11377 * option, that option will appear to be selected.
11378 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11381 * After the user chooses an option, its `data` will be used as a new value for the widget.
11382 * A `label` also can be specified for each option: if given, it will be shown instead of the
11383 * `data` in the dropdown menu.
11385 * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
11387 * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
11390 * // A ComboBoxInputWidget.
11391 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11392 * value: 'Option 1',
11394 * { data: 'Option 1' },
11395 * { data: 'Option 2' },
11396 * { data: 'Option 3' }
11399 * $( document.body ).append( comboBox.$element );
11402 * // Example: A ComboBoxInputWidget with additional option labels.
11403 * var comboBox = new OO.ui.ComboBoxInputWidget( {
11404 * value: 'Option 1',
11407 * data: 'Option 1',
11408 * label: 'Option One'
11411 * data: 'Option 2',
11412 * label: 'Option Two'
11415 * data: 'Option 3',
11416 * label: 'Option Three'
11420 * $( document.body ).append( comboBox.$element );
11422 * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
11425 * @extends OO.ui.TextInputWidget
11428 * @param {Object} [config] Configuration options
11429 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11430 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11431 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11432 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11433 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11434 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11436 OO
.ui
.ComboBoxInputWidget
= function OoUiComboBoxInputWidget( config
) {
11437 // Configuration initialization
11438 config
= $.extend( {
11439 autocomplete
: false
11442 // ComboBoxInputWidget shouldn't support `multiline`
11443 config
.multiline
= false;
11445 // See InputWidget#reusePreInfuseDOM about `config.$input`
11446 if ( config
.$input
) {
11447 config
.$input
.removeAttr( 'list' );
11450 // Parent constructor
11451 OO
.ui
.ComboBoxInputWidget
.parent
.call( this, config
);
11454 this.$overlay
= ( config
.$overlay
=== true ? OO
.ui
.getDefaultOverlay() : config
.$overlay
) || this.$element
;
11455 this.dropdownButton
= new OO
.ui
.ButtonWidget( {
11456 classes
: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
11457 label
: OO
.ui
.msg( 'ooui-combobox-button-label' ),
11459 invisibleLabel
: true,
11460 disabled
: this.disabled
11462 this.menu
= new OO
.ui
.MenuSelectWidget( $.extend(
11466 $floatableContainer
: this.$element
,
11467 disabled
: this.isDisabled()
11473 this.connect( this, {
11474 change
: 'onInputChange',
11475 enter
: 'onInputEnter'
11477 this.dropdownButton
.connect( this, {
11478 click
: 'onDropdownButtonClick'
11480 this.menu
.connect( this, {
11481 choose
: 'onMenuChoose',
11482 add
: 'onMenuItemsChange',
11483 remove
: 'onMenuItemsChange',
11484 toggle
: 'onMenuToggle'
11488 this.$input
.attr( {
11490 'aria-owns': this.menu
.getElementId(),
11491 'aria-autocomplete': 'list'
11493 this.dropdownButton
.$button
.attr( {
11494 'aria-controls': this.menu
.getElementId()
11496 // Do not override options set via config.menu.items
11497 if ( config
.options
!== undefined ) {
11498 this.setOptions( config
.options
);
11500 this.$field
= $( '<div>' )
11501 .addClass( 'oo-ui-comboBoxInputWidget-field' )
11502 .append( this.$input
, this.dropdownButton
.$element
);
11504 .addClass( 'oo-ui-comboBoxInputWidget' )
11505 .append( this.$field
);
11506 this.$overlay
.append( this.menu
.$element
);
11507 this.onMenuItemsChange();
11512 OO
.inheritClass( OO
.ui
.ComboBoxInputWidget
, OO
.ui
.TextInputWidget
);
11517 * Get the combobox's menu.
11519 * @return {OO.ui.MenuSelectWidget} Menu widget
11521 OO
.ui
.ComboBoxInputWidget
.prototype.getMenu = function () {
11526 * Get the combobox's text input widget.
11528 * @return {OO.ui.TextInputWidget} Text input widget
11530 OO
.ui
.ComboBoxInputWidget
.prototype.getInput = function () {
11535 * Handle input change events.
11538 * @param {string} value New value
11540 OO
.ui
.ComboBoxInputWidget
.prototype.onInputChange = function ( value
) {
11541 var match
= this.menu
.findItemFromData( value
);
11543 this.menu
.selectItem( match
);
11544 if ( this.menu
.findHighlightedItem() ) {
11545 this.menu
.highlightItem( match
);
11548 if ( !this.isDisabled() ) {
11549 this.menu
.toggle( true );
11554 * Handle input enter events.
11558 OO
.ui
.ComboBoxInputWidget
.prototype.onInputEnter = function () {
11559 if ( !this.isDisabled() ) {
11560 this.menu
.toggle( false );
11565 * Handle button click events.
11569 OO
.ui
.ComboBoxInputWidget
.prototype.onDropdownButtonClick = function () {
11570 this.menu
.toggle();
11575 * Handle menu choose events.
11578 * @param {OO.ui.OptionWidget} item Chosen item
11580 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuChoose = function ( item
) {
11581 this.setValue( item
.getData() );
11585 * Handle menu item change events.
11589 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuItemsChange = function () {
11590 var match
= this.menu
.findItemFromData( this.getValue() );
11591 this.menu
.selectItem( match
);
11592 if ( this.menu
.findHighlightedItem() ) {
11593 this.menu
.highlightItem( match
);
11595 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu
.isEmpty() );
11599 * Handle menu toggle events.
11602 * @param {boolean} isVisible Open state of the menu
11604 OO
.ui
.ComboBoxInputWidget
.prototype.onMenuToggle = function ( isVisible
) {
11605 this.$element
.toggleClass( 'oo-ui-comboBoxInputWidget-open', isVisible
);
11611 OO
.ui
.ComboBoxInputWidget
.prototype.setDisabled = function ( disabled
) {
11613 OO
.ui
.ComboBoxInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
11615 if ( this.dropdownButton
) {
11616 this.dropdownButton
.setDisabled( this.isDisabled() );
11619 this.menu
.setDisabled( this.isDisabled() );
11626 * Set the options available for this input.
11628 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11630 * @return {OO.ui.Widget} The widget, for chaining
11632 OO
.ui
.ComboBoxInputWidget
.prototype.setOptions = function ( options
) {
11635 .addItems( options
.map( function ( opt
) {
11636 return new OO
.ui
.MenuOptionWidget( {
11638 label
: opt
.label
!== undefined ? opt
.label
: opt
.data
11646 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
11647 * which is a widget that is specified by reference before any optional configuration settings.
11649 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
11651 * - **left**: The label is placed before the field-widget and aligned with the left margin.
11652 * A left-alignment is used for forms with many fields.
11653 * - **right**: The label is placed before the field-widget and aligned to the right margin.
11654 * A right-alignment is used for long but familiar forms which users tab through,
11655 * verifying the current field with a quick glance at the label.
11656 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
11657 * that users fill out from top to bottom.
11658 * - **inline**: The label is placed after the field-widget and aligned to the left.
11659 * An inline-alignment is best used with checkboxes or radio buttons.
11661 * Help text can either be:
11663 * - accessed via a help icon that appears in the upper right corner of the rendered field layout, or
11664 * - shown as a subtle explanation below the label.
11666 * If the help text is brief, or is essential to always expose it, set `helpInline` to `true`. If it
11667 * is long or not essential, leave `helpInline` to its default, `false`.
11669 * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
11671 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
11674 * @extends OO.ui.Layout
11675 * @mixins OO.ui.mixin.LabelElement
11676 * @mixins OO.ui.mixin.TitledElement
11679 * @param {OO.ui.Widget} fieldWidget Field widget
11680 * @param {Object} [config] Configuration options
11681 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
11683 * @cfg {Array} [errors] Error messages about the widget, which will be
11684 * displayed below the widget.
11685 * The array may contain strings or OO.ui.HtmlSnippet instances.
11686 * @cfg {Array} [notices] Notices about the widget, which will be displayed
11687 * below the widget.
11688 * The array may contain strings or OO.ui.HtmlSnippet instances.
11689 * These are more visible than `help` messages when `helpInline` is set, and so
11690 * might be good for transient messages.
11691 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
11692 * and `helpInline` is `false`, a "help" icon will appear in the upper-right
11693 * corner of the rendered field; clicking it will display the text in a popup.
11694 * If `helpInline` is `true`, then a subtle description will be shown after the
11696 * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
11697 * or shown when the "help" icon is clicked.
11698 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
11700 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
11702 * @throws {Error} An error is thrown if no widget is specified
11704 OO
.ui
.FieldLayout
= function OoUiFieldLayout( fieldWidget
, config
) {
11705 // Allow passing positional parameters inside the config object
11706 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
11707 config
= fieldWidget
;
11708 fieldWidget
= config
.fieldWidget
;
11711 // Make sure we have required constructor arguments
11712 if ( fieldWidget
=== undefined ) {
11713 throw new Error( 'Widget not found' );
11716 // Configuration initialization
11717 config
= $.extend( { align
: 'left', helpInline
: false }, config
);
11719 // Parent constructor
11720 OO
.ui
.FieldLayout
.parent
.call( this, config
);
11722 // Mixin constructors
11723 OO
.ui
.mixin
.LabelElement
.call( this, $.extend( {}, config
, {
11724 $label
: $( '<label>' )
11726 OO
.ui
.mixin
.TitledElement
.call( this, $.extend( {}, config
, { $titled
: this.$label
} ) );
11729 this.fieldWidget
= fieldWidget
;
11732 this.$field
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
11733 this.$messages
= $( '<ul>' );
11734 this.$header
= $( '<span>' );
11735 this.$body
= $( '<div>' );
11737 this.helpInline
= config
.helpInline
;
11740 this.fieldWidget
.connect( this, { disable
: 'onFieldDisable' } );
11743 this.$help
= config
.help
?
11744 this.createHelpElement( config
.help
, config
.$overlay
) :
11746 if ( this.fieldWidget
.getInputId() ) {
11747 this.$label
.attr( 'for', this.fieldWidget
.getInputId() );
11748 if ( this.helpInline
) {
11749 this.$help
.attr( 'for', this.fieldWidget
.getInputId() );
11752 this.$label
.on( 'click', function () {
11753 this.fieldWidget
.simulateLabelClick();
11755 if ( this.helpInline
) {
11756 this.$help
.on( 'click', function () {
11757 this.fieldWidget
.simulateLabelClick();
11762 .addClass( 'oo-ui-fieldLayout' )
11763 .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget
.isDisabled() )
11764 .append( this.$body
);
11765 this.$body
.addClass( 'oo-ui-fieldLayout-body' );
11766 this.$header
.addClass( 'oo-ui-fieldLayout-header' );
11767 this.$messages
.addClass( 'oo-ui-fieldLayout-messages' );
11769 .addClass( 'oo-ui-fieldLayout-field' )
11770 .append( this.fieldWidget
.$element
);
11772 this.setErrors( config
.errors
|| [] );
11773 this.setNotices( config
.notices
|| [] );
11774 this.setAlignment( config
.align
);
11775 // Call this again to take into account the widget's accessKey
11776 this.updateTitle();
11781 OO
.inheritClass( OO
.ui
.FieldLayout
, OO
.ui
.Layout
);
11782 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.LabelElement
);
11783 OO
.mixinClass( OO
.ui
.FieldLayout
, OO
.ui
.mixin
.TitledElement
);
11788 * Handle field disable events.
11791 * @param {boolean} value Field is disabled
11793 OO
.ui
.FieldLayout
.prototype.onFieldDisable = function ( value
) {
11794 this.$element
.toggleClass( 'oo-ui-fieldLayout-disabled', value
);
11798 * Get the widget contained by the field.
11800 * @return {OO.ui.Widget} Field widget
11802 OO
.ui
.FieldLayout
.prototype.getField = function () {
11803 return this.fieldWidget
;
11807 * Return `true` if the given field widget can be used with `'inline'` alignment (see
11808 * #setAlignment). Return `false` if it can't or if this can't be determined.
11810 * @return {boolean}
11812 OO
.ui
.FieldLayout
.prototype.isFieldInline = function () {
11813 // This is very simplistic, but should be good enough.
11814 return this.getField().$element
.prop( 'tagName' ).toLowerCase() === 'span';
11819 * @param {string} kind 'error' or 'notice'
11820 * @param {string|OO.ui.HtmlSnippet} text
11823 OO
.ui
.FieldLayout
.prototype.makeMessage = function ( kind
, text
) {
11824 var $listItem
, $icon
, message
;
11825 $listItem
= $( '<li>' );
11826 if ( kind
=== 'error' ) {
11827 $icon
= new OO
.ui
.IconWidget( { icon
: 'alert', flags
: [ 'warning' ] } ).$element
;
11828 $listItem
.attr( 'role', 'alert' );
11829 } else if ( kind
=== 'notice' ) {
11830 $icon
= new OO
.ui
.IconWidget( { icon
: 'notice' } ).$element
;
11834 message
= new OO
.ui
.LabelWidget( { label
: text
} );
11836 .append( $icon
, message
.$element
)
11837 .addClass( 'oo-ui-fieldLayout-messages-' + kind
);
11842 * Set the field alignment mode.
11845 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
11847 * @return {OO.ui.BookletLayout} The layout, for chaining
11849 OO
.ui
.FieldLayout
.prototype.setAlignment = function ( value
) {
11850 if ( value
!== this.align
) {
11851 // Default to 'left'
11852 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value
) === -1 ) {
11856 if ( value
=== 'inline' && !this.isFieldInline() ) {
11859 // Reorder elements
11861 if ( this.helpInline
) {
11862 if ( value
=== 'top' ) {
11863 this.$header
.append( this.$label
);
11864 this.$body
.append( this.$header
, this.$field
, this.$help
);
11865 } else if ( value
=== 'inline' ) {
11866 this.$header
.append( this.$label
, this.$help
);
11867 this.$body
.append( this.$field
, this.$header
);
11869 this.$header
.append( this.$label
, this.$help
);
11870 this.$body
.append( this.$header
, this.$field
);
11873 if ( value
=== 'top' ) {
11874 this.$header
.append( this.$help
, this.$label
);
11875 this.$body
.append( this.$header
, this.$field
);
11876 } else if ( value
=== 'inline' ) {
11877 this.$header
.append( this.$help
, this.$label
);
11878 this.$body
.append( this.$field
, this.$header
);
11880 this.$header
.append( this.$label
);
11881 this.$body
.append( this.$header
, this.$help
, this.$field
);
11884 // Set classes. The following classes can be used here:
11885 // * oo-ui-fieldLayout-align-left
11886 // * oo-ui-fieldLayout-align-right
11887 // * oo-ui-fieldLayout-align-top
11888 // * oo-ui-fieldLayout-align-inline
11889 if ( this.align
) {
11890 this.$element
.removeClass( 'oo-ui-fieldLayout-align-' + this.align
);
11892 this.$element
.addClass( 'oo-ui-fieldLayout-align-' + value
);
11893 this.align
= value
;
11900 * Set the list of error messages.
11902 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
11903 * The array may contain strings or OO.ui.HtmlSnippet instances.
11905 * @return {OO.ui.BookletLayout} The layout, for chaining
11907 OO
.ui
.FieldLayout
.prototype.setErrors = function ( errors
) {
11908 this.errors
= errors
.slice();
11909 this.updateMessages();
11914 * Set the list of notice messages.
11916 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
11917 * The array may contain strings or OO.ui.HtmlSnippet instances.
11919 * @return {OO.ui.BookletLayout} The layout, for chaining
11921 OO
.ui
.FieldLayout
.prototype.setNotices = function ( notices
) {
11922 this.notices
= notices
.slice();
11923 this.updateMessages();
11928 * Update the rendering of error and notice messages.
11932 OO
.ui
.FieldLayout
.prototype.updateMessages = function () {
11934 this.$messages
.empty();
11936 if ( this.errors
.length
|| this.notices
.length
) {
11937 this.$body
.after( this.$messages
);
11939 this.$messages
.remove();
11943 for ( i
= 0; i
< this.notices
.length
; i
++ ) {
11944 this.$messages
.append( this.makeMessage( 'notice', this.notices
[ i
] ) );
11946 for ( i
= 0; i
< this.errors
.length
; i
++ ) {
11947 this.$messages
.append( this.makeMessage( 'error', this.errors
[ i
] ) );
11952 * Include information about the widget's accessKey in our title. TitledElement calls this method.
11953 * (This is a bit of a hack.)
11956 * @param {string} title Tooltip label for 'title' attribute
11959 OO
.ui
.FieldLayout
.prototype.formatTitleWithAccessKey = function ( title
) {
11960 if ( this.fieldWidget
&& this.fieldWidget
.formatTitleWithAccessKey
) {
11961 return this.fieldWidget
.formatTitleWithAccessKey( title
);
11967 * Creates and returns the help element. Also sets the `aria-describedby`
11968 * attribute on the main element of the `fieldWidget`.
11971 * @param {string|OO.ui.HtmlSnippet} [help] Help text.
11972 * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
11973 * @return {jQuery} The element that should become `this.$help`.
11975 OO
.ui
.FieldLayout
.prototype.createHelpElement = function ( help
, $overlay
) {
11976 var helpId
, helpWidget
;
11978 if ( this.helpInline
) {
11979 helpWidget
= new OO
.ui
.LabelWidget( {
11981 classes
: [ 'oo-ui-inline-help' ]
11984 helpId
= helpWidget
.getElementId();
11986 helpWidget
= new OO
.ui
.PopupButtonWidget( {
11987 $overlay
: $overlay
,
11991 classes
: [ 'oo-ui-fieldLayout-help' ],
11994 label
: OO
.ui
.msg( 'ooui-field-help' ),
11995 invisibleLabel
: true
11997 if ( help
instanceof OO
.ui
.HtmlSnippet
) {
11998 helpWidget
.getPopup().$body
.html( help
.toString() );
12000 helpWidget
.getPopup().$body
.text( help
);
12003 helpId
= helpWidget
.getPopup().getBodyId();
12006 // Set the 'aria-describedby' attribute on the fieldWidget
12007 // Preference given to an input or a button
12009 this.fieldWidget
.$input
||
12010 this.fieldWidget
.$button
||
12011 this.fieldWidget
.$element
12012 ).attr( 'aria-describedby', helpId
);
12014 return helpWidget
.$element
;
12018 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
12019 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
12020 * is required and is specified before any optional configuration settings.
12022 * Labels can be aligned in one of four ways:
12024 * - **left**: The label is placed before the field-widget and aligned with the left margin.
12025 * A left-alignment is used for forms with many fields.
12026 * - **right**: The label is placed before the field-widget and aligned to the right margin.
12027 * A right-alignment is used for long but familiar forms which users tab through,
12028 * verifying the current field with a quick glance at the label.
12029 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
12030 * that users fill out from top to bottom.
12031 * - **inline**: The label is placed after the field-widget and aligned to the left.
12032 * An inline-alignment is best used with checkboxes or radio buttons.
12034 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
12035 * text is specified.
12038 * // Example of an ActionFieldLayout
12039 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
12040 * new OO.ui.TextInputWidget( {
12041 * placeholder: 'Field widget'
12043 * new OO.ui.ButtonWidget( {
12047 * label: 'An ActionFieldLayout. This label is aligned top',
12049 * help: 'This is help text'
12053 * $( document.body ).append( actionFieldLayout.$element );
12056 * @extends OO.ui.FieldLayout
12059 * @param {OO.ui.Widget} fieldWidget Field widget
12060 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
12061 * @param {Object} config
12063 OO
.ui
.ActionFieldLayout
= function OoUiActionFieldLayout( fieldWidget
, buttonWidget
, config
) {
12064 // Allow passing positional parameters inside the config object
12065 if ( OO
.isPlainObject( fieldWidget
) && config
=== undefined ) {
12066 config
= fieldWidget
;
12067 fieldWidget
= config
.fieldWidget
;
12068 buttonWidget
= config
.buttonWidget
;
12071 // Parent constructor
12072 OO
.ui
.ActionFieldLayout
.parent
.call( this, fieldWidget
, config
);
12075 this.buttonWidget
= buttonWidget
;
12076 this.$button
= $( '<span>' );
12077 this.$input
= this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
12081 .addClass( 'oo-ui-actionFieldLayout' );
12083 .addClass( 'oo-ui-actionFieldLayout-button' )
12084 .append( this.buttonWidget
.$element
);
12086 .addClass( 'oo-ui-actionFieldLayout-input' )
12087 .append( this.fieldWidget
.$element
);
12089 .append( this.$input
, this.$button
);
12094 OO
.inheritClass( OO
.ui
.ActionFieldLayout
, OO
.ui
.FieldLayout
);
12097 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
12098 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
12099 * configured with a label as well. For more information and examples,
12100 * please see the [OOUI documentation on MediaWiki][1].
12103 * // Example of a fieldset layout
12104 * var input1 = new OO.ui.TextInputWidget( {
12105 * placeholder: 'A text input field'
12108 * var input2 = new OO.ui.TextInputWidget( {
12109 * placeholder: 'A text input field'
12112 * var fieldset = new OO.ui.FieldsetLayout( {
12113 * label: 'Example of a fieldset layout'
12116 * fieldset.addItems( [
12117 * new OO.ui.FieldLayout( input1, {
12118 * label: 'Field One'
12120 * new OO.ui.FieldLayout( input2, {
12121 * label: 'Field Two'
12124 * $( document.body ).append( fieldset.$element );
12126 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
12129 * @extends OO.ui.Layout
12130 * @mixins OO.ui.mixin.IconElement
12131 * @mixins OO.ui.mixin.LabelElement
12132 * @mixins OO.ui.mixin.GroupElement
12135 * @param {Object} [config] Configuration options
12136 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
12137 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
12138 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
12139 * For important messages, you are advised to use `notices`, as they are always shown.
12140 * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
12141 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
12143 OO
.ui
.FieldsetLayout
= function OoUiFieldsetLayout( config
) {
12144 // Configuration initialization
12145 config
= config
|| {};
12147 // Parent constructor
12148 OO
.ui
.FieldsetLayout
.parent
.call( this, config
);
12150 // Mixin constructors
12151 OO
.ui
.mixin
.IconElement
.call( this, config
);
12152 OO
.ui
.mixin
.LabelElement
.call( this, config
);
12153 OO
.ui
.mixin
.GroupElement
.call( this, config
);
12156 this.$header
= $( '<legend>' );
12157 if ( config
.help
) {
12158 this.popupButtonWidget
= new OO
.ui
.PopupButtonWidget( {
12159 $overlay
: config
.$overlay
,
12163 classes
: [ 'oo-ui-fieldsetLayout-help' ],
12166 label
: OO
.ui
.msg( 'ooui-field-help' ),
12167 invisibleLabel
: true
12169 if ( config
.help
instanceof OO
.ui
.HtmlSnippet
) {
12170 this.popupButtonWidget
.getPopup().$body
.html( config
.help
.toString() );
12172 this.popupButtonWidget
.getPopup().$body
.text( config
.help
);
12174 this.$help
= this.popupButtonWidget
.$element
;
12176 this.$help
= $( [] );
12181 .addClass( 'oo-ui-fieldsetLayout-header' )
12182 .append( this.$icon
, this.$label
, this.$help
);
12183 this.$group
.addClass( 'oo-ui-fieldsetLayout-group' );
12185 .addClass( 'oo-ui-fieldsetLayout' )
12186 .prepend( this.$header
, this.$group
);
12187 if ( Array
.isArray( config
.items
) ) {
12188 this.addItems( config
.items
);
12194 OO
.inheritClass( OO
.ui
.FieldsetLayout
, OO
.ui
.Layout
);
12195 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.IconElement
);
12196 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.LabelElement
);
12197 OO
.mixinClass( OO
.ui
.FieldsetLayout
, OO
.ui
.mixin
.GroupElement
);
12199 /* Static Properties */
12205 OO
.ui
.FieldsetLayout
.static.tagName
= 'fieldset';
12208 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
12209 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
12210 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
12211 * See the [OOUI documentation on MediaWiki] [1] for more information and examples.
12213 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
12214 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
12215 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
12216 * some fancier controls. Some controls have both regular and InputWidget variants, for example
12217 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
12218 * often have simplified APIs to match the capabilities of HTML forms.
12219 * See the [OOUI documentation on MediaWiki] [2] for more information about InputWidgets.
12221 * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Forms
12222 * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
12225 * // Example of a form layout that wraps a fieldset layout
12226 * var input1 = new OO.ui.TextInputWidget( {
12227 * placeholder: 'Username'
12229 * var input2 = new OO.ui.TextInputWidget( {
12230 * placeholder: 'Password',
12233 * var submit = new OO.ui.ButtonInputWidget( {
12237 * var fieldset = new OO.ui.FieldsetLayout( {
12238 * label: 'A form layout'
12240 * fieldset.addItems( [
12241 * new OO.ui.FieldLayout( input1, {
12242 * label: 'Username',
12245 * new OO.ui.FieldLayout( input2, {
12246 * label: 'Password',
12249 * new OO.ui.FieldLayout( submit )
12251 * var form = new OO.ui.FormLayout( {
12252 * items: [ fieldset ],
12253 * action: '/api/formhandler',
12256 * $( document.body ).append( form.$element );
12259 * @extends OO.ui.Layout
12260 * @mixins OO.ui.mixin.GroupElement
12263 * @param {Object} [config] Configuration options
12264 * @cfg {string} [method] HTML form `method` attribute
12265 * @cfg {string} [action] HTML form `action` attribute
12266 * @cfg {string} [enctype] HTML form `enctype` attribute
12267 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
12269 OO
.ui
.FormLayout
= function OoUiFormLayout( config
) {
12272 // Configuration initialization
12273 config
= config
|| {};
12275 // Parent constructor
12276 OO
.ui
.FormLayout
.parent
.call( this, config
);
12278 // Mixin constructors
12279 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12282 this.$element
.on( 'submit', this.onFormSubmit
.bind( this ) );
12284 // Make sure the action is safe
12285 action
= config
.action
;
12286 if ( action
!== undefined && !OO
.ui
.isSafeUrl( action
) ) {
12287 action
= './' + action
;
12292 .addClass( 'oo-ui-formLayout' )
12294 method
: config
.method
,
12296 enctype
: config
.enctype
12298 if ( Array
.isArray( config
.items
) ) {
12299 this.addItems( config
.items
);
12305 OO
.inheritClass( OO
.ui
.FormLayout
, OO
.ui
.Layout
);
12306 OO
.mixinClass( OO
.ui
.FormLayout
, OO
.ui
.mixin
.GroupElement
);
12311 * A 'submit' event is emitted when the form is submitted.
12316 /* Static Properties */
12322 OO
.ui
.FormLayout
.static.tagName
= 'form';
12327 * Handle form submit events.
12330 * @param {jQuery.Event} e Submit event
12332 * @return {OO.ui.FormLayout} The layout, for chaining
12334 OO
.ui
.FormLayout
.prototype.onFormSubmit = function () {
12335 if ( this.emit( 'submit' ) ) {
12341 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
12342 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
12345 * // Example of a panel layout
12346 * var panel = new OO.ui.PanelLayout( {
12350 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
12352 * $( document.body ).append( panel.$element );
12355 * @extends OO.ui.Layout
12358 * @param {Object} [config] Configuration options
12359 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
12360 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
12361 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
12362 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
12364 OO
.ui
.PanelLayout
= function OoUiPanelLayout( config
) {
12365 // Configuration initialization
12366 config
= $.extend( {
12373 // Parent constructor
12374 OO
.ui
.PanelLayout
.parent
.call( this, config
);
12377 this.$element
.addClass( 'oo-ui-panelLayout' );
12378 if ( config
.scrollable
) {
12379 this.$element
.addClass( 'oo-ui-panelLayout-scrollable' );
12381 if ( config
.padded
) {
12382 this.$element
.addClass( 'oo-ui-panelLayout-padded' );
12384 if ( config
.expanded
) {
12385 this.$element
.addClass( 'oo-ui-panelLayout-expanded' );
12387 if ( config
.framed
) {
12388 this.$element
.addClass( 'oo-ui-panelLayout-framed' );
12394 OO
.inheritClass( OO
.ui
.PanelLayout
, OO
.ui
.Layout
);
12399 * Focus the panel layout
12401 * The default implementation just focuses the first focusable element in the panel
12403 OO
.ui
.PanelLayout
.prototype.focus = function () {
12404 OO
.ui
.findFocusable( this.$element
).focus();
12408 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
12409 * items), with small margins between them. Convenient when you need to put a number of block-level
12410 * widgets on a single line next to each other.
12412 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
12415 * // HorizontalLayout with a text input and a label
12416 * var layout = new OO.ui.HorizontalLayout( {
12418 * new OO.ui.LabelWidget( { label: 'Label' } ),
12419 * new OO.ui.TextInputWidget( { value: 'Text' } )
12422 * $( document.body ).append( layout.$element );
12425 * @extends OO.ui.Layout
12426 * @mixins OO.ui.mixin.GroupElement
12429 * @param {Object} [config] Configuration options
12430 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
12432 OO
.ui
.HorizontalLayout
= function OoUiHorizontalLayout( config
) {
12433 // Configuration initialization
12434 config
= config
|| {};
12436 // Parent constructor
12437 OO
.ui
.HorizontalLayout
.parent
.call( this, config
);
12439 // Mixin constructors
12440 OO
.ui
.mixin
.GroupElement
.call( this, $.extend( {}, config
, { $group
: this.$element
} ) );
12443 this.$element
.addClass( 'oo-ui-horizontalLayout' );
12444 if ( Array
.isArray( config
.items
) ) {
12445 this.addItems( config
.items
);
12451 OO
.inheritClass( OO
.ui
.HorizontalLayout
, OO
.ui
.Layout
);
12452 OO
.mixinClass( OO
.ui
.HorizontalLayout
, OO
.ui
.mixin
.GroupElement
);
12455 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12456 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
12457 * (to adjust the value in increments) to allow the user to enter a number.
12460 * // A NumberInputWidget.
12461 * var numberInput = new OO.ui.NumberInputWidget( {
12462 * label: 'NumberInputWidget',
12463 * input: { value: 5 },
12467 * $( document.body ).append( numberInput.$element );
12470 * @extends OO.ui.TextInputWidget
12473 * @param {Object} [config] Configuration options
12474 * @cfg {Object} [minusButton] Configuration options to pass to the
12475 * {@link OO.ui.ButtonWidget decrementing button widget}.
12476 * @cfg {Object} [plusButton] Configuration options to pass to the
12477 * {@link OO.ui.ButtonWidget incrementing button widget}.
12478 * @cfg {number} [min=-Infinity] Minimum allowed value
12479 * @cfg {number} [max=Infinity] Maximum allowed value
12480 * @cfg {number|null} [step] If specified, the field only accepts values that are multiples of this.
12481 * @cfg {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12482 * Defaults to `step` if specified, otherwise `1`.
12483 * @cfg {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12484 * Defaults to 10 times `buttonStep`.
12485 * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
12487 OO
.ui
.NumberInputWidget
= function OoUiNumberInputWidget( config
) {
12488 var $field
= $( '<div>' )
12489 .addClass( 'oo-ui-numberInputWidget-field' );
12491 // Configuration initialization
12492 config
= $.extend( {
12498 // For backward compatibility
12499 $.extend( config
, config
.input
);
12502 // Parent constructor
12503 OO
.ui
.NumberInputWidget
.parent
.call( this, $.extend( config
, {
12507 if ( config
.showButtons
) {
12508 this.minusButton
= new OO
.ui
.ButtonWidget( $.extend(
12510 disabled
: this.isDisabled(),
12512 classes
: [ 'oo-ui-numberInputWidget-minusButton' ],
12517 this.minusButton
.$element
.attr( 'aria-hidden', 'true' );
12518 this.plusButton
= new OO
.ui
.ButtonWidget( $.extend(
12520 disabled
: this.isDisabled(),
12522 classes
: [ 'oo-ui-numberInputWidget-plusButton' ],
12527 this.plusButton
.$element
.attr( 'aria-hidden', 'true' );
12532 keydown
: this.onKeyDown
.bind( this ),
12533 'wheel mousewheel DOMMouseScroll': this.onWheel
.bind( this )
12535 if ( config
.showButtons
) {
12536 this.plusButton
.connect( this, {
12537 click
: [ 'onButtonClick', +1 ]
12539 this.minusButton
.connect( this, {
12540 click
: [ 'onButtonClick', -1 ]
12545 $field
.append( this.$input
);
12546 if ( config
.showButtons
) {
12548 .prepend( this.minusButton
.$element
)
12549 .append( this.plusButton
.$element
);
12553 if ( config
.allowInteger
|| config
.isInteger
) {
12554 // Backward compatibility
12557 this.setRange( config
.min
, config
.max
);
12558 this.setStep( config
.buttonStep
, config
.pageStep
, config
.step
);
12559 // Set the validation method after we set step and range
12560 // so that it doesn't immediately call setValidityFlag
12561 this.setValidation( this.validateNumber
.bind( this ) );
12564 .addClass( 'oo-ui-numberInputWidget' )
12565 .toggleClass( 'oo-ui-numberInputWidget-buttoned', config
.showButtons
)
12571 OO
.inheritClass( OO
.ui
.NumberInputWidget
, OO
.ui
.TextInputWidget
);
12575 // Backward compatibility
12576 OO
.ui
.NumberInputWidget
.prototype.setAllowInteger = function ( flag
) {
12577 this.setStep( flag
? 1 : null );
12579 // Backward compatibility
12580 OO
.ui
.NumberInputWidget
.prototype.setIsInteger
= OO
.ui
.NumberInputWidget
.prototype.setAllowInteger
;
12582 // Backward compatibility
12583 OO
.ui
.NumberInputWidget
.prototype.getAllowInteger = function () {
12584 return this.step
=== 1;
12586 // Backward compatibility
12587 OO
.ui
.NumberInputWidget
.prototype.getIsInteger
= OO
.ui
.NumberInputWidget
.prototype.getAllowInteger
;
12590 * Set the range of allowed values
12592 * @param {number} min Minimum allowed value
12593 * @param {number} max Maximum allowed value
12595 OO
.ui
.NumberInputWidget
.prototype.setRange = function ( min
, max
) {
12597 throw new Error( 'Minimum (' + min
+ ') must not be greater than maximum (' + max
+ ')' );
12601 this.$input
.attr( 'min', this.min
);
12602 this.$input
.attr( 'max', this.max
);
12603 this.setValidityFlag();
12607 * Get the current range
12609 * @return {number[]} Minimum and maximum values
12611 OO
.ui
.NumberInputWidget
.prototype.getRange = function () {
12612 return [ this.min
, this.max
];
12616 * Set the stepping deltas
12618 * @param {number} [buttonStep=step||1] Delta when using the buttons or up/down arrow keys.
12619 * Defaults to `step` if specified, otherwise `1`.
12620 * @param {number} [pageStep=10*buttonStep] Delta when using the page-up/page-down keys.
12621 * Defaults to 10 times `buttonStep`.
12622 * @param {number|null} [step] If specified, the field only accepts values that are multiples of this.
12624 OO
.ui
.NumberInputWidget
.prototype.setStep = function ( buttonStep
, pageStep
, step
) {
12625 if ( buttonStep
=== undefined ) {
12626 buttonStep
= step
|| 1;
12628 if ( pageStep
=== undefined ) {
12629 pageStep
= 10 * buttonStep
;
12631 if ( step
!== null && step
<= 0 ) {
12632 throw new Error( 'Step value, if given, must be positive' );
12634 if ( buttonStep
<= 0 ) {
12635 throw new Error( 'Button step value must be positive' );
12637 if ( pageStep
<= 0 ) {
12638 throw new Error( 'Page step value must be positive' );
12641 this.buttonStep
= buttonStep
;
12642 this.pageStep
= pageStep
;
12643 this.$input
.attr( 'step', this.step
|| 'any' );
12644 this.setValidityFlag();
12650 OO
.ui
.NumberInputWidget
.prototype.setValue = function ( value
) {
12651 if ( value
=== '' ) {
12652 // Some browsers allow a value in the input even if there isn't one reported by $input.val()
12653 // so here we make sure an 'empty' value is actually displayed as such.
12654 this.$input
.val( '' );
12656 return OO
.ui
.NumberInputWidget
.parent
.prototype.setValue
.call( this, value
);
12660 * Get the current stepping values
12662 * @return {number[]} Button step, page step, and validity step
12664 OO
.ui
.NumberInputWidget
.prototype.getStep = function () {
12665 return [ this.buttonStep
, this.pageStep
, this.step
];
12669 * Get the current value of the widget as a number
12671 * @return {number} May be NaN, or an invalid number
12673 OO
.ui
.NumberInputWidget
.prototype.getNumericValue = function () {
12674 return +this.getValue();
12678 * Adjust the value of the widget
12680 * @param {number} delta Adjustment amount
12682 OO
.ui
.NumberInputWidget
.prototype.adjustValue = function ( delta
) {
12683 var n
, v
= this.getNumericValue();
12686 if ( isNaN( delta
) || !isFinite( delta
) ) {
12687 throw new Error( 'Delta must be a finite number' );
12690 if ( isNaN( v
) ) {
12694 n
= Math
.max( Math
.min( n
, this.max
), this.min
);
12696 n
= Math
.round( n
/ this.step
) * this.step
;
12701 this.setValue( n
);
12708 * @param {string} value Field value
12709 * @return {boolean}
12711 OO
.ui
.NumberInputWidget
.prototype.validateNumber = function ( value
) {
12713 if ( value
=== '' ) {
12714 return !this.isRequired();
12717 if ( isNaN( n
) || !isFinite( n
) ) {
12721 if ( this.step
&& Math
.floor( n
/ this.step
) !== n
/ this.step
) {
12725 if ( n
< this.min
|| n
> this.max
) {
12733 * Handle mouse click events.
12736 * @param {number} dir +1 or -1
12738 OO
.ui
.NumberInputWidget
.prototype.onButtonClick = function ( dir
) {
12739 this.adjustValue( dir
* this.buttonStep
);
12743 * Handle mouse wheel events.
12746 * @param {jQuery.Event} event
12747 * @return {undefined/boolean} False to prevent default if event is handled
12749 OO
.ui
.NumberInputWidget
.prototype.onWheel = function ( event
) {
12752 if ( !this.isDisabled() && this.$input
.is( ':focus' ) ) {
12753 // Standard 'wheel' event
12754 if ( event
.originalEvent
.deltaMode
!== undefined ) {
12755 this.sawWheelEvent
= true;
12757 if ( event
.originalEvent
.deltaY
) {
12758 delta
= -event
.originalEvent
.deltaY
;
12759 } else if ( event
.originalEvent
.deltaX
) {
12760 delta
= event
.originalEvent
.deltaX
;
12763 // Non-standard events
12764 if ( !this.sawWheelEvent
) {
12765 if ( event
.originalEvent
.wheelDeltaX
) {
12766 delta
= -event
.originalEvent
.wheelDeltaX
;
12767 } else if ( event
.originalEvent
.wheelDeltaY
) {
12768 delta
= event
.originalEvent
.wheelDeltaY
;
12769 } else if ( event
.originalEvent
.wheelDelta
) {
12770 delta
= event
.originalEvent
.wheelDelta
;
12771 } else if ( event
.originalEvent
.detail
) {
12772 delta
= -event
.originalEvent
.detail
;
12777 delta
= delta
< 0 ? -1 : 1;
12778 this.adjustValue( delta
* this.buttonStep
);
12786 * Handle key down events.
12789 * @param {jQuery.Event} e Key down event
12790 * @return {undefined/boolean} False to prevent default if event is handled
12792 OO
.ui
.NumberInputWidget
.prototype.onKeyDown = function ( e
) {
12793 if ( !this.isDisabled() ) {
12794 switch ( e
.which
) {
12795 case OO
.ui
.Keys
.UP
:
12796 this.adjustValue( this.buttonStep
);
12798 case OO
.ui
.Keys
.DOWN
:
12799 this.adjustValue( -this.buttonStep
);
12801 case OO
.ui
.Keys
.PAGEUP
:
12802 this.adjustValue( this.pageStep
);
12804 case OO
.ui
.Keys
.PAGEDOWN
:
12805 this.adjustValue( -this.pageStep
);
12814 OO
.ui
.NumberInputWidget
.prototype.setDisabled = function ( disabled
) {
12816 OO
.ui
.NumberInputWidget
.parent
.prototype.setDisabled
.call( this, disabled
);
12818 if ( this.minusButton
) {
12819 this.minusButton
.setDisabled( this.isDisabled() );
12821 if ( this.plusButton
) {
12822 this.plusButton
.setDisabled( this.isDisabled() );
12830 //# sourceMappingURL=oojs-ui-core.js.map.json